Java backend geliştirme ekipleri için idempotent POST uçlarını PostgreSQL, Spring Boot ve gözlemlenebilirlik araçlarıyla tasarlayın. Tekrarlanan isteklerde çift ödeme, çift sipariş ve belirsiz retry etkilerini yönetin.
Spring Boot'ta Idempotent API ve Java Microservices Tasarımı
Java Microservices içinde idempotency sözleşmesini netleştirmek
Bir POST isteğini idempotent yapmak, yalnızca Idempotency-Key başlığını kabul etmek değildir. İstemci aynı anahtarı aynı iş niyeti için yeniden göndermeli; sunucu da anahtarın daha önce farklı bir gövdeyle kullanıldığını ayırt etmelidir. Java backend geliştirme için pratik sözleşme şudur: anahtar 128 bit veya daha yüksek rastgelelikte UUID olmalı, tekrar denemede aynı HTTP gövdesi byte dizisi gönderilmeli ve aynı anahtar farklı gövdeyle gelirse 409 Conflict dönmelidir. Bu yaklaşım, ağ zaman aşımında istemcinin sonucun işlenip işlenmediğini bilememesi durumunda güvenli retry sağlar.
İlk sorgusunu yapan bir java eğitimi veya java programlama eğitimi katılımcısı için kritik ayrıntı, HTTP metodunun idempotent görünmesinin yeterli olmamasıdır. Örneğin ödeme sağlayıcısına çağrı yapan POST /payments, istemci timeout sonrası tekrarlandığında iki ayrı sağlayıcı işlemi yaratabilir. İstemci tarafında curl ile aynı anahtarı test edin; ikinci çağrının aynı kaynak kimliğini ve aynı durum kodunu döndürmesi gerekir.
KEY=$(uuidgen)
curl -i -X POST http://localhost:8080/payments -H "Content-Type: application/json" -H "Idempotency-Key: $KEY" -d '{"orderId":"o-42","amount":1250,"currency":"TRY"}'
curl -i -X POST http://localhost:8080/payments -H "Content-Type: application/json" -H "Idempotency-Key: $KEY" -d '{"orderId":"o-42","amount":1250,"currency":"TRY"}'Anahtarı yalnızca cache anahtarı gibi ele almak yaygın hatadır. Redis'te TTL dolduktan sonra aynı anahtarın tekrar işlenmesi, ödeme veya sipariş gibi uzun ömürlü iş kurallarında çift etkiye yol açar. Kalıcılık süresini iş alanına göre belirleyin: örneğin kart provizyonunun mutabakat penceresi 7 günse, idempotency kaydını en az 7 gün saklayın. PostgreSQL'de saklama maliyetini kontrol etmek için created_at < now() - interval '7 days' koşuluyla günlük batch silme yapın ve silmeden önce denetim gereksinimini doğrulayın.
Spring Framework ve PostgreSQL ile atomik istek sahiplenme
Eşzamanlı iki isteğin ikisini de işleme almasını önlemek için uygulama içi synchronized kullanmayın; birden çok pod ve yeniden başlatma senaryosunda koruma kaybolur. PostgreSQL unique constraint ve INSERT ... ON CONFLICT DO NOTHING kombinasyonu, sahiplenmeyi veritabanı seviyesinde atomik yapar. Flyway ile aşağıdaki migration'ı çalıştırın; request_hash aynı anahtarın farklı payload ile kullanılmasını saptar.
CREATE TABLE idempotency_record (
idempotency_key varchar(128) PRIMARY KEY,
request_hash bytea NOT NULL,
state varchar(16) NOT NULL,
response_status integer,
response_body jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
completed_at timestamptz
);
CREATE INDEX ix_idempotency_record_created_at
ON idempotency_record (created_at);Aşağıdaki JdbcTemplate örneğinde ilk istek kaydı ekler ve iş mantığını çalıştırır. Rakip istek, unique index nedeniyle ilk transaction commit olana kadar bekler; ardından kaydı okur ve tamamlanmış cevabı döndürür. Bu nedenle 'önce SELECT, sonra INSERT' akışı yarış koşuluna açıktır, tek başına kullanılmamalıdır.
@Transactional
public ResponseEntity<String> createPayment(String key, byte[] rawBody) {
byte[] hash = MessageDigest.getInstance("SHA-256").digest(rawBody);
int claimed = jdbc.update("""
INSERT INTO idempotency_record(idempotency_key, request_hash, state)
VALUES (?, ?, 'IN_PROGRESS')
ON CONFLICT (idempotency_key) DO NOTHING
""", key, hash);
if (claimed == 0) {
IdempotencyRecord old = repository.findById(key).orElseThrow();
if (!MessageDigest.isEqual(old.requestHash(), hash)) {
throw new ResponseStatusException(HttpStatus.CONFLICT,
"Idempotency-Key was used with another payload");
}
if ("COMPLETED".equals(old.state())) {
return ResponseEntity.status(old.responseStatus()).body(old.responseBody());
}
throw new ResponseStatusException(HttpStatus.CONFLICT,
"Request is still being processed; retry with the same key");
}
PaymentResult result = paymentService.charge(rawBody);
jdbc.update("""
UPDATE idempotency_record
SET state = 'COMPLETED', response_status = ?, response_body = ?::jsonb,
completed_at = now()
WHERE idempotency_key = ?
""", result.status(), result.json(), key);
return ResponseEntity.status(result.status()).body(result.json());
}Bu kodda hash'i raw body üzerinden hesaplamak bilinçli bir sözleşmedir: JSON alan sırası değişen ama semantik olarak aynı iki gövde 409 alır. İstemci retry mekanizması normalde aynı byte dizisini tekrar gönderdiği için bu davranış deterministiktir. 'Semantik eşitlik' istiyorsanız Jackson ile ağaç modelini kanonikleştirmeniz gerekir; ancak 1 ve 1.0, Unicode normalizasyonu ve tarih biçimleri için ayrıca açık kurallar yazmadan yalnızca alan sıralamak güvenli değildir.
Spring Data JPA ve Hibernate ORM ile transaction sınırları
Spring Data JPA veya Hibernate ORM kullanılan projelerde entity kaydını save() ile ekleyip unique ihlalini Java'da yakalamak cazip görünür. Ancak JPA flush işlemini transaction sonuna erteleyebilir; hatayı beklediğiniz satırda değil commit sırasında alırsınız. Bu akışta atomic claim için native SQL veya JDBC kullanmak daha nettir. Hibernate entity'yi yalnızca kayıt okuma ve yönetim ekranları için kullanın; PostgreSQL'e özgü ON CONFLICT ifadesini repository native query ile çağıracaksanız dönüş değerini mutlaka kontrol edin.
Dış sistem çağrısını veritabanı transaction'ı içinde doğrudan yapmak da dikkat ister. Transaction rollback olursa idempotency kaydı silinir, fakat ödeme sağlayıcısı çağrısı başarıyla tamamlanmış olabilir. Bu pencereyi kapatmak için yerel transaction içinde ödeme niyetini ve outbox olayını yazın, sonra ayrı worker sağlayıcıyı çağırıp sonucu güncellesin. Böylece retry aynı payment intent'i görür. Spring Framework transaction senkronizasyonu yerine transactional outbox tablosunu tercih etmenin nedeni, JVM çöküşünden sonra afterCommit callback'inin çalışacağının garanti edilmemesidir.
CREATE TABLE outbox_event (
id uuid PRIMARY KEY,
aggregate_id varchar(64) NOT NULL,
event_type varchar(80) NOT NULL,
payload jsonb NOT NULL,
published_at timestamptz
);
-- Worker sahiplenmesi: birden fazla worker aynı olayı alamaz
SELECT id, payload
FROM outbox_event
WHERE published_at IS NULL
ORDER BY id
FOR UPDATE SKIP LOCKED
LIMIT 100;Bir spring boot eğitimi laboratuvarında bunu test ederken test veritabanını H2 ile değiştirmeyin. H2'nin PostgreSQL uyumluluk modu, unique conflict bekleme davranışını ve jsonb tipini üretimle aynı modellemez. Testcontainers PostgreSQL ile iki paralel istek gönderin ve yalnızca bir adet payment_intent satırı oluştuğunu doğrulayın. Bu, java kursu projelerinde görülen 'tek thread testte geçti' yanılgısını yakalar.
Java backend geliştirme için idempotency gecikmesini ölçmek
Idempotency tablosu büyüdükçe sorun genellikle business kodunda değil, unique index beklemesinde görünür. Micrometer ile anahtarın kendisini etiketlemeyin; her anahtar ayrı time series üreterek Prometheus cardinality sınırını tüketir. Bunun yerine yalnızca outcome=claimed|replayed|conflict etiketiyle sayaç tutun ve claim süresini timer olarak kaydedin.
Timer.Sample sample = Timer.start(meterRegistry);
try {
ResponseEntity<String> response = service.createPayment(key, rawBody);
meterRegistry.counter("idempotency.requests", "outcome", "claimed").increment();
return response;
} catch (ResponseStatusException ex) {
meterRegistry.counter("idempotency.requests", "outcome", "conflict").increment();
throw ex;
} finally {
sample.stop(Timer.builder("idempotency.claim.latency")
.publishPercentileHistogram()
.register(meterRegistry));
}Önce-sonra karşılaştırmasını ölçmeden unique index veya cache eklemeyin. k6 ile aynı anahtarın yüzde 10 oranında tekrarlandığı yük üretin, ardından p95 idempotency.claim.latency, PostgreSQL lock wait ve duplicate payment sayısını karşılaştırın. İlk ölçümde 'SELECT sonra INSERT' akışında duplicate kayıt veya 23505 hatası görüyorsanız, atomic insert sonrasında duplicate sayısı sıfır olmalı; buna karşılık yoğun aynı-key yükünde p95 bekleme süresi artabilir. Bu bekleme, iki iş mantığı çalıştırmaktan daha doğru bir maliyettir.
import http from 'k6/http';
import { check } from 'k6';
export const options = { vus: 40, duration: '30s' };
export default function () {
const key = __VU % 10 === 0 ? 'retry-key-42' : crypto.randomUUID();
const res = http.post('http://localhost:8080/payments',
JSON.stringify({ orderId: 'o-' + __ITER, amount: 1250 }),
{ headers: { 'Content-Type': 'application/json', 'Idempotency-Key': key } });
check(res, { 'status is expected': r => [200, 201, 409].includes(r.status) });
}Sorgu planını gerçek veride incelemek için PostgreSQL'de pg_stat_statements ve EXPLAIN (ANALYZE, BUFFERS) kullanın. Birçok ekip yalnızca primary key lookup beklerken TTL temizleme işinin created_at indeksini kullanmadığını fark eder. Aşağıdaki komutta Seq Scan ve yüksek shared read block görürseniz batch boyutunu küçültün veya zaman indeksini doğrulayın.
EXPLAIN (ANALYZE, BUFFERS)
DELETE FROM idempotency_record
WHERE created_at < now() - interval '7 days';
SELECT query, calls, mean_exec_time
FROM pg_stat_statements
WHERE query LIKE '%idempotency_record%'
ORDER BY mean_exec_time DESC
LIMIT 10;Spring AI, Spring MCP ve Model Context Protocol çağrılarında tekrar deneme
Spring AI kullanan bir ajan, araç çağrısını modelin kararına göre yeniden deneyebilir; Spring MCP üzerinden bir Model Context Protocol aracı da ağ hatasında aynı tool invocation'ı yeniden gönderebilir. Araç tanımında istemcinin ürettiği idempotency anahtarını zorunlu şema alanı yapın ve bunu downstream ödeme veya sipariş API'sine aynen aktarın. Tool çağrısının doğal dil açıklamasına güvenmek yerine JSON Schema ile requestId biçimini doğrulayın.
{
"name": "create_payment",
"inputSchema": {
"type": "object",
"required": ["requestId", "orderId", "amount"],
"properties": {
"requestId": { "type": "string", "format": "uuid" },
"orderId": { "type": "string", "minLength": 1 },
"amount": { "type": "integer", "minimum": 1 }
}
}
}Buradaki edge case, modelin aynı kullanıcı isteği için yeni UUID üretmesidir. Yeni UUID yeni business intent gibi yorumlanacağı için idempotency koruması devre dışı kalır. Gateway, kullanıcı oturumu ve onaylanmış iş niyetinden türetilen bir requestId üretmeli; ajan yalnızca bu değeri iletmelidir. Tool sonucuna paymentId ve replayed alanlarını koyun, böylece ajan ikinci çağrıda yeni bir ödeme üretmek yerine önceki sonucu bağlama ekleyebilir.
Bu konu, java fullstack eğitimi içinde frontend retry davranışını da kapsar: React veya mobil istemci, 5xx ya da timeout sonrası yeni anahtar üretmemeli ve anahtarı kullanıcı işlemi tamamlanana kadar state içinde saklamalıdır. Spring AI, spring mcp ve klasik REST istemcilerinin hepsi aynı kontratı izlediğinde, java microservices zincirinde retry kaynaklı yan etkiyi API katmanında izole etmiş olursunuz.
İlgili Eğitim
YTÜSEM İlgili Eğitim
Java Spring Boot ReactJS FullStack Eğitimi (Yıldız Teknik Üniversitesi SEM)
Sık Sorulan Sorular
Spring Boot'ta Idempotency-Key için Redis mi PostgreSQL mi kullanmalıyım?
Ödeme, sipariş ve dış sağlayıcı çağrısı gibi kalıcı iş etkilerinde PostgreSQL unique constraint kullanın; aynı transaction içinde iş kaydı ve idempotency kaydı yazılabilir. Redis ancak kısa ömürlü istek bastırma katmanı olarak eklenmelidir. Redis TTL dolduğunda tekrar işleme riski varsa, nihai karar kaynağı yine kalıcı veritabanı olmalıdır.
Spring Data JPA ile idempotent POST yarış koşulu nasıl test edilir?
Testcontainers PostgreSQL başlatın, aynı Idempotency-Key ile iki paralel HTTP isteğini CountDownLatch veya JUnit ExecutorService ile eşzamanlı gönderin. Assertion olarak iki cevabın aynı paymentId'yi taşıdığını ve payment_intent tablosunda COUNT(*) değerinin 1 olduğunu kontrol edin. H2 üzerinde bu testi çalıştırmak PostgreSQL conflict ve lock davranışını güvenilir biçimde temsil etmez.
Java backend geliştirme sırasında idempotency performansı nasıl profillenir?
Micrometer ile idempotency.claim.latency histogramını ve claimed/replayed/conflict sayaçlarını yayınlayın. k6 ile tekrar eden anahtarlı yük üretin; önce SELECT-then-INSERT, sonra INSERT ON CONFLICT DO NOTHING akışlarında p95 gecikme, 23505 hata sayısı ve duplicate business kayıt sayısını karşılaştırın. PostgreSQL tarafında pg_stat_statements ve EXPLAIN (ANALYZE, BUFFERS) ile index ve temizleme sorgularını inceleyin.
Spring AI ve Spring MCP araçları idempotent istek anahtarını nasıl taşımalı?
Model Context Protocol tool input şemasında requestId alanını UUID olarak zorunlu yapın. Gateway bu değeri kullanıcı aksiyonu başında bir kez üretmeli, Spring AI veya Spring MCP tool'u aynı değeri downstream Idempotency-Key başlığına aktarmalıdır. Modelin her retry için yeni UUID üretmesine izin vermeyin; bu, her denemeyi yeni iş niyeti haline getirir.
AI / LLM Discovery
Bu makale Opendart Akademi Java eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.


