Idempotency etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster
Idempotency etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster

26 Mart 2026 Perşembe

Yazılım Mimarisi - Idempotency ve Phantom Write

Giriş
Açıklaması şöyle
You typically implement idempotency like this:
  1. Check if request already processed (via key / timestamp / PK)
  2. If not → write data
  3. If yes → skip
Eğer check işlemi atomic değilse problem oluyor.

Failure Mode 1: The TTL Expiry Trap
Açıklaması şöyle
The most common idempotency implementation stores a request key with a time-to-live (TTL) — typically 24 or 48 hours. The assumption is that any duplicate will arrive within that window. In practice, this assumption frequently breaks.
Açıklaması şöyle
The fix: Never use TTL-only idempotency for operations with unbounded retry windows. Instead, use a database-backed idempotency store with a three-state model (IN_PROGRESS, COMPLETED, FAILED) where the expires_at column drives a cleanup job for storage management — not correctness. The cleanup window should be set significantly longer than your worst-case replay window (7 days minimum for Kafka-based systems).
Failure Mode 2: The Partial Execution Ghost
Açıklaması şöyle
A request arrives, the system writes the idempotency key with status IN_PROGRESS, begins processing, writes half the data, and crashes — JVM OOM, container eviction, network partition. The idempotency key is now in IN_PROGRESS state. When the retry arrives, the system faces an impossible decision: did the original operation complete or not?
Açıklaması şöyle
The fix: Wrap both the business logic and the idempotency state transition in a single database transaction. If the transaction rolls back, both the business data and the idempotency status roll back together. For stale IN_PROGRESS keys (where the original processor is likely dead), use a configurable timeout threshold to reclaim and re-execute safely.
Failure Mode 3: The Concurrent Check Race
Burada check koşulu atomic değil. Açıklaması şöyle
The fix: Use INSERT ... ON CONFLICT DO NOTHING (PostgreSQL 9.5+) to make the check-and-claim atomic. If the RETURNING clause yields no rows, the key already existed — fetch its status with SELECT ... FOR UPDATE. For non-blocking behavior, SELECT ... FOR UPDATE SKIP LOCKED lets the second instance return 409 Conflict immediately rather than waiting.
Failure Mode 4: The Layer Mismatch
Açıklaması şöyle
The fix: Propagate a correlation ID from the original request as a Kafka header, and have every downstream consumer enforce its own idempotency barrier using that ID as the deduplication key.
Spring Boot + SQL Server
Kod şöyle. Burada 
-  Partial Execution tek transaction ile çözülüyor.
- The Concurrent Check Race, DuplicateKeyException ile çözülüyor. Eğer Postgres kullanıyor olsaydık exception yerine SQL'in kaç tane satırı değiştirdiğine bakacaktır
- The Layer Mismatch sorunu outbox pattern ile çözülüyor.
@Service
@RequiredArgsConstructor
public class IdempotentService {
  private final JdbcTemplate jdbc;
  public record Response(String result) {}

  @Transactional
  public Response handleRequest(String idempotencyKey, String payload) {
    try {
      // Attempt barrier insert (atomic)
      // SQL Server:
      // INSERT INTO idempotency_table (idempotency_key, status)
      // VALUES (?, 'IN_PROGRESS')
      jdbc.update(
        "INSERT INTO idempotency_table (idempotency_key, status) VALUES (?, 'IN_PROGRESS')",
        idempotencyKey
      );

      // First request owns the key → perform business logic
      String result = doBusinessLogic(payload);

      // Insert into outbox for async processing
      // SQL Server:
      // INSERT INTO outbox_table (idempotency_key, payload) VALUES (?, ?)
      jdbc.update(
        "INSERT INTO outbox_table (idempotency_key, payload) VALUES (?, ?)",
        idempotencyKey, result
      );

      // Mark barrier as completed and store result
      // SQL Server:
      // UPDATE idempotency_table SET status='COMPLETED', response=? WHERE idempotency_key=?
      jdbc.update(
        "UPDATE idempotency_table SET status='COMPLETED', response=? WHERE idempotency_key=?",
        result, idempotencyKey
      );
      return new Response(result);
     } catch (DuplicateKeyException ex) {
      // Barrier row already exists → handle duplicate
       // SQL Server:
       // SELECT * FROM idempotency_table WITH (UPDLOCK, ROWLOCK) WHERE idempotency_key=?
       IdempotencyRecord record = jdbc.queryForObject(
         "SELECT status, response FROM idempotency_table WITH (UPDLOCK, ROWLOCK) WHERE idempotency_key=?",
         (rs, rowNum) -> new IdempotencyRecord(rs.getString("status"), rs.getString("response")),
         idempotencyKey
       );

       switch (record.status) {
         case "COMPLETED":
           // Return cached result
           return new Response(record.response);
         case "IN_PROGRESS":
           // Someone else is working → can wait or throw 409
           throw new IllegalStateException("Request is already in progress");
         case "FAILED":
           // Previous attempt failed → allow retry
           throw new IllegalStateException("Previous attempt failed, safe to retry");
         default:
           throw new IllegalStateException("Unknown barrier state: " + record.status);
         }
      }
  }

  private String doBusinessLogic(String payload) {
    // your domain logic here
    return "processed:" + payload;
  }

  private static class IdempotencyRecord {
      final String status;
      final String response;
      IdempotencyRecord(String status, String response) {
        this.status = status;
        this.response = response;
      }
  }
}
Eğer hem SQL Server hem de Postgres için çalışsın istiyorsak şöyle yaparızz
    
    
@Service
@RequiredArgsConstructor
public class IdempotentService {

    private final JdbcTemplate jdbc;

    public record Response(String result) {}

    @Transactional
    public Response handleRequest(String idempotencyKey, String payload) {
        boolean isWinner = false;

        try {
            // --------------------------
            // Attempt atomic barrier insert
            // --------------------------
            // Postgres:
            // INSERT INTO idempotency_table (idempotency_key, status)
            // VALUES (?, 'IN_PROGRESS')
            // ON CONFLICT DO NOTHING
            //
            // SQL Server:
            // INSERT INTO idempotency_table (idempotency_key, status)
            // VALUES (?, 'IN_PROGRESS')
            int rows = jdbc.update(
                    "INSERT INTO idempotency_table (idempotency_key, status) VALUES (?, 'IN_PROGRESS')",
                    idempotencyKey
            );

            // Postgres: rows == 1 → winner
            // SQL Server: INSERT succeeded → winner
            isWinner = rows == 1;

        } catch (DuplicateKeyException ex) {
            // SQL Server only: duplicate → loser
            isWinner = false;
        }

        if (isWinner) {
            // --------------------------
            // Winner executes business logic
            // --------------------------
            String result = doBusinessLogic(payload);

            // Insert into outbox (side effect)
            // INSERT INTO outbox_table (idempotency_key, payload) VALUES (?, ?)
            jdbc.update(
                    "INSERT INTO outbox_table (idempotency_key, payload) VALUES (?, ?)",
                    idempotencyKey, result
            );

            // Mark barrier as completed + store response
            // UPDATE idempotency_table SET status='COMPLETED', response=? WHERE idempotency_key=?
            jdbc.update(
                    "UPDATE idempotency_table SET status='COMPLETED', response=? WHERE idempotency_key=?",
                    result, idempotencyKey
            );

            return new Response(result);
        } else {
            // --------------------------
            // Loser reads existing row safely
            // --------------------------
            // SQL Server: SELECT ... WITH (UPDLOCK, ROWLOCK) WHERE idempotency_key=?
            // Postgres: SELECT * FROM idempotency_table WHERE idempotency_key=?
            IdempotencyRecord record = jdbc.queryForObject(
                    "SELECT status, response FROM idempotency_table " +
                            (isPostgres() ? "" : "WITH (UPDLOCK, ROWLOCK) ") +
                            "WHERE idempotency_key=?",
                    (rs, rowNum) -> new IdempotencyRecord(rs.getString("status"), rs.getString("response")),
                    idempotencyKey
            );

            switch (record.status) {
                case "COMPLETED":
                    return new Response(record.response);
                case "IN_PROGRESS":
                    throw new IllegalStateException("Request already in progress");
                case "FAILED":
                    throw new IllegalStateException("Previous attempt failed, safe to retry");
                default:
                    throw new IllegalStateException("Unknown barrier state: " + record.status);
            }
        }
    }

    private boolean isPostgres() {
        // Detect DB type from DataSource or JdbcTemplate if needed
        return true; // placeholder, implement detection
    }

    private String doBusinessLogic(String payload) {
        return "processed:" + payload;
    }

    private static class IdempotencyRecord {
        final String status;
        final String response;

        IdempotencyRecord(String status, String response) {
            this.status = status;
            this.response = response;
        }
    }
}


2 Kasım 2020 Pazartesi

Yazılım Mimarisi - Idempotency (Denkgüçlülük) Nedir

Giriş
Idempotency kelimesinin Türkçe karşılığı da biraz tuhaf aslında :)


Idempotency şu anlama gelir. Bir metodun başarıyla çalıştıktan sonra tekrar çağrılması durumunda, yeni bir değişiklik yapmamasıdır. Sistemin Idempotency özelliği mikroservis (mikroservice) mimarisi ile daha çok konuşulur oldu.

En çok kullanılan örneklerden birisi şöyle
In more complex micro service processes, the code executing when a client clicks the "Purchase button", typically invokes several micro services. Imagine something as follows.

1. User clicks the purchase button
2. Money is deducted from the client's account
3. Money is transferred to the manufacturer's account
4. The order is handled internally, and shipping is started

If step 3 or step 4 fails in the above process, you cannot simply rerun the process, because this would result in deducting the client's bank account twice. If all of the above invocations are idempotent though, the process can easily be re-animated by simply running it in its entirety once more.
Idempotency Yöntemleri
Idempotency için iki tane yöntem var.

1. Her şeyin en baştan yapılması
Bu yöntemin kullanılabilmesi için her şeyin en baştan yapılmasının zararsız olması gerekir. Örneğin bir web sayfası, sayfadaki bir bileşen değiştikçe tekrar tekrar render edilebilir. Sayfa en baştan render edildiği için zararsızdır.

2. Sadece başarısız adımların baştan yapılması - Tekil Numara İle Takip Edilir
Örneğin banka hesabından para çekilip, e-posta gönderilmesi işlemi olsun. Hesaptan para çekme işlemi başarılı, ancak e-posta gönderme işlemi başarısız ise, metod tekrar çağrıldığında sadece e-posta gönderme işlemi yapılmalıdır. Bu yöntemi gerçekleştirmek için kullanılacak yöntemlerden birisi de işlem başına bir tekil numara kullanılması. Açıklaması şöyle
Idempotency is actually very easy to implement. Just create a new Guid or Uuid, and associate with every invocation to your micro service invocations, and if the micro service endpoint encounters an identifier it has previously executed, it returns early, and doesn't apply any state changes to its internal states. This of course requires persisting the identifier for the execution somehow, but that's fairly simple in practice. If the client calling the endpoint requires access to the returned value, you can also persist the return value of some micro service process, and return the previous result, looking it up using the invocation identification as a criteria.
Örnek
Açıklaması şöylee
... we can maintain an INBOX table inside the consumer service’s database. It simply keeps track of what events were processed by recording their UUIDs.
After processing an event for the first time, the consumer marks the event as processed in the INBOX table. That should be transactional — making it possible to trap any rollbacks at the consumer level so that it can retry receiving the event.
Şeklen şöyle

Örnek
Kod olarak şuna benzer
function decrementInventoryStockCount(txid, pid, offset) {
  transaction {
    tx_executed = check transaction table record where id=txid
      if not tx_executed {
        prod_count = select count from inventory where product=pid
        prod_count += offset
        set count=prod_count in inventory where product=pid
        insert to transaction table with id=txid
      }
  }
}
Burada dikkat edilmesi gerek şeyler şöyle
1. Metod bir txid  yan işlem numarasını parametre olarak alıyor
2. Metod bir transaction içinde çalışıyor

txid kullanılmasının açıklaması şöyle
Sometimes it’s not feasible to make data operations idempotent due to transaction isolation issues we get in a microservices communication. This means we can’t blindly retry operations in a transaction if we are not sure whether an operation was executed in a remote microservice. This can be solved by having a unique identifier such as a transaction ID for the operation of the microservice call, so the target microservice will create a history of transactions executed against it. In this manner, for each microservice operation call, it can do a local transaction to check the history and see if this transaction has already been executed. If not, it will execute the database operation, and still in the local transaction, update the transaction history table as well.
HTTP ve Idempotency
Açıklaması şöyle. HTTP için dikkat edilmesi gereken şey POST istekleri.
RFC 2616 says that methods GET, PUT and DELETE should be idempotent. That means aside from error or expiration issues, the side-effects for multiple requests are the same as for a single request. So if I issue 1 GET request or 2 or 3 of them with the same request headers and body, the net result is going to be the exactly the same. The same idea applies to PUT and DELETE requests.
..
But if you read RFC 2616 you’ll notice that they left out the POST method. This method is intended to have side-effects since the intention was to use it to create new objects.

Idempotency'nin Önem Kazandığı Bazı Örnekler

1. Observer Tasarım Örüntüsü ve Idempotency
Observer tetiklenince idempotent olarak tabir edilen ve her şeyi baştan yapan bir metod çağırır. Açıklaması şöyle
Let's say you have an HTML page that is fairly complicated-- if you pick something in one dropdown, another control might appear, or the values in a third control might change. There's two ways you could approach this:

  1. Write a separate handler, for each and every control, that respond to events on that control, and updates other controls as needed.
  2. Write a single handler that looks at the state of all the controls on the page and just fixes everything.
The second call is "idempotent" because you can call it over and over again and the controls will always be arranged properly. Whereas the first call(s) may have issues if a call is lost or repeated, e.g. if one of the handlers performs a toggle.

The logic for the second call would be a bit more obscure, but you only have to write one handler.

And you can always use both solutions, calling the "fix everything" function as needed "just to be on the safe side."
2. Retry ve Idempotency
Bir işlem başarısız olduğunda tekrar denemek (retry) isteyebilir. Tekrar deneme yönteminde karşı tarafın Idempotent Receiver olması gerekir. Açıklaması şöyle.
One thing that you need to be mindful when retrying is message idempotency. What happens if we get an HTTP timeout when calling the Fan Courier HTTP API, but our shipment request was actually processed successfully, we just didn't get the response back? When we retry, we don't want to send a new shipment. This is why the Fan Courier Gateway needs to be an Idempotent Receiver. This means that it doesn't matter if it processes the same message only once or 5 times, the result will always be the same: a single shipment request.