Monday, 26 October 2026

VPC vs VPN

 

  • VPC vs subnet

  • Public subnet vs private subnet

  • Security Group vs network ACL

  • Internet Gateway vs NAT Gateway

  • VPC peering vs Transit Gateway

  • VPN vs VPC

  • VPC endpoints for S3 and DynamoDB

  • Multi-AZ architecture

  • Monday, 5 October 2026

    Idempotency

     

    The client sends the Idempotency-Key to the server. The server generates/stores the result associated with that key.

    Flow

    Client
       |
       | POST /payments
       | Idempotency-Key: abc-123-xyz
       | amount = ₹10,000
       ↓
    Payment API
       |
       | Check: Have I already processed abc-123-xyz?
       |
       ├── NO → Process payment
       |        Save key + result
       |
       └── YES → Return previous result

    First request

    The client generates a unique key:

    POST /payments
    Idempotency-Key: abc-123-xyz
    Content-Type: application/json
    {
      "amount": 10000,
      "currency": "INR"
    }

    Server processes it:

    abc-123-xyz → PAY-1001 → SUCCESS

    The server stores something like:

    idempotency_key = abc-123-xyz
    payment_id      = PAY-1001
    status          = SUCCESS

    If network fails

    The client doesn't know whether payment succeeded, so it sends the same key:

    POST /payments
    Idempotency-Key: abc-123-xyz

    Server checks:

    abc-123-xyz exists?
            ↓
          YES
            ↓
    Don't process payment again
            ↓
    Return PAY-1001

    So:

    Request 1 → abc-123-xyz → Payment PAY-1001 created
    Request 2 → abc-123-xyz → Existing PAY-1001 returned
    Request 3 → abc-123-xyz → Existing PAY-1001 returned

    Important point

    Client generates the key; server is responsible for enforcing it.

    Usually the client generates a UUID:

    550e8400-e29b-41d4-a716-446655440000

    For example, in a frontend:

    const idempotencyKey = crypto.randomUUID();
    
    fetch("/payments", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "Idempotency-Key": idempotencyKey
        },
        body: JSON.stringify({
            amount: 10000,
            currency: "INR"
        })
    });

    The same key must be reused when retrying the same logical payment. A new key means:

    new key → server assumes new operation

    This is why you should not generate a new key for every retry.

    show how backend accept validate and return same key if fail, if pass does it return empty

    Yes. The backend accepts the key from the client, validates it, checks whether it was already processed, and then returns the payment result.

    It does not normally return an empty response just because the key is valid.

    Typical Spring Boot flow

    Client
      |
      | POST /payments
      | Idempotency-Key: abc-123
      | {amount: 10000}
      ↓
    Controller
      |
      | 1. Validate key
      | 2. Check DB
      ↓
    ┌─────────────────────────────┐
    │ Does abc-123 already exist? │
    └─────────────────────────────┘
           |              |
          YES             NO
           |              |
           ↓              ↓
    Return saved       Process payment
    response            |
                        ↓
                    Save key + result
                        |
                        ↓
                    Return result

    Example Controller

    @PostMapping("/payments")
    public ResponseEntity<PaymentResponse> createPayment(
            @RequestHeader("Idempotency-Key") String idempotencyKey,
            @RequestBody PaymentRequest request) {
    
        // 1. Validate key
        if (idempotencyKey == null || idempotencyKey.isBlank()) {
            return ResponseEntity.badRequest().build();
        }
    
        // 2. Check whether this key was already processed
        Optional<IdempotencyRecord> existing =
                idempotencyRepository.findByKey(idempotencyKey);
    
        if (existing.isPresent()) {
            // Retry → return SAME result
            return ResponseEntity.ok(existing.get().getPaymentResponse());
        }
    
        // 3. First request → process payment
        PaymentResponse response = paymentService.processPayment(request);
    
        // 4. Store key + result
        idempotencyRepository.save(
            new IdempotencyRecord(
                idempotencyKey,
                response
            )
        );
    
        // 5. Return result
        return ResponseEntity.ok(response);
    }

    First request

    Client sends:

    POST /payments
    Idempotency-Key: abc-123

    Backend:

    abc-123 exists? → NO
           ↓
    Process payment
           ↓
    PAY-1001 created
           ↓
    Save:
    abc-123 → PAY-1001 SUCCESS
           ↓
    Return:
    {
      "paymentId": "PAY-1001",
      "status": "SUCCESS"
    }

    Retry because client didn't receive response

    Client sends the same key:

    POST /payments
    Idempotency-Key: abc-123

    Backend:

    abc-123 exists? → YES
           ↓
    DO NOT process payment again
           ↓
    Return stored response
    {
      "paymentId": "PAY-1001",
      "status": "SUCCESS"
    }

    So the client receives the same payment result.


    What if the key is valid but payment fails?

    Suppose:

    abc-123 → Payment processing failed

    You have a design choice.

    For many payment systems, you store the final result associated with the key, including an appropriate failure outcome, so retries with the same key don't accidentally create a different operation.

    For example:

    {
      "paymentId": "PAY-1001",
      "status": "FAILED",
      "reason": "Insufficient funds"
    }

    A retry using abc-123 can return that same result.


    One very important backend detail

    You need a unique constraint on the idempotency key:

    CREATE TABLE idempotency_keys (
        id BIGINT PRIMARY KEY,
        idempotency_key VARCHAR(255) UNIQUE,
        payment_id VARCHAR(100),
        status VARCHAR(50),
        response JSON
    );

    The UNIQUE constraint is important because two requests could arrive at almost exactly the same time:

    Request A ──→ abc-123 ──→ check DB → not found
    Request B ──→ abc-123 ──→ check DB → not found

    Without proper concurrency handling, both could process the payment.

    That's where real-world idempotency becomes more interesting: you need atomic insert / unique constraint + transaction/locking strategy, not just an if (exists) check.

    No. @Idempotent is not a built-in Spring Boot annotation.

    If you see code like:

    @Idempotent
    @PostMapping("/payments")
    public PaymentResponse createPayment(...) {
        ...
    }

    then @Idempotent is almost certainly:

    • a custom annotation created by the application, or
    • provided by a third-party library.

    Spring Boot itself does not provide a general-purpose @Idempotent annotation for REST APIs.

    In Spring Boot

    You could create your own:

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Idempotent {
    }

    Then use it:

    @Idempotent
    @PostMapping("/payments")
    public PaymentResponse createPayment(...) {
        // payment logic
    }

    But the annotation alone does nothing.

    You would need something such as Spring AOP:

    @Idempotent
         ↓
    Spring AOP intercepts method
         ↓
    Read Idempotency-Key
         ↓
    Check Redis/DB
         ↓
    Already exists?
       YES → return previous response
       NO  → execute method
              ↓
           save response

    So if you see @Idempotent in a project, don't assume it's Spring Boot. Check its import:

    import ....Idempotent;

    That import will tell you exactly where it comes from.

    @Idempotent create realtime AOP where i use response store and request validation

    Yes. Let's build a real Spring Boot AOP-based idempotency implementation where:

    1. Client sends Idempotency-Key
    2. AOP intercepts methods marked @Idempotent
    3. Request/key is validated
    4. Redis checks whether the key was already processed
    5. If already processed → return the same stored response
    6. If not → execute the actual business method
    7. Store the response in Redis
    8. Return the response

    For a real application, I would use Redis, because it is fast and supports atomic operations.


    1. Architecture

                     Client
                       |
                       | POST /payments
                       | Idempotency-Key: abc-123
                       ↓
            ┌──────────────────────┐
            │ Spring Controller    │
            │ @Idempotent          │
            └──────────┬───────────┘
                       |
                       ↓
            ┌──────────────────────┐
            │     AOP Aspect       │
            │                      │
            │ Validate key         │
            │ Check Redis          │
            └──────────┬───────────┘
                       |
              ┌────────┴────────┐
              ↓                 ↓
          Key exists         Key doesn't exist
              |                 |
              ↓                 ↓
       Return stored       Execute method
          response              |
                                ↓
                         Payment Service
                                |
                                ↓
                             Database
                                |
                                ↓
                         PaymentResponse
                                |
                                ↓
                          Store in Redis
                                |
                                ↓
                           Return response

    2. Maven dependencies

    For Spring Boot 3/4 style applications, conceptually you need:

    <dependencies>
    
        <!-- Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    
        <!-- AOP -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
    
        <!-- Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
    
    </dependencies>

    3. Create @Idempotent

    This is our custom annotation.

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Idempotent {
    
        /**
         * How long the response should remain cached.
         */
        long ttlSeconds() default 86400;
    }

    Here:

    @Idempotent

    means:

    "Apply idempotency handling to this API."


    4. Request DTO

    public record PaymentRequest(
            BigDecimal amount,
            String currency,
            String customerId
    ) {
    }

    Example request:

    {
      "amount": 10000,
      "currency": "INR",
      "customerId": "CUST-101"
    }

    5. Response DTO

    public record PaymentResponse(
            String paymentId,
            String status,
            BigDecimal amount
    ) {
    }

    Example:

    {
      "paymentId": "PAY-1001",
      "status": "SUCCESS",
      "amount": 10000
    }

    6. Redis configuration

    spring:
      data:
        redis:
          host: localhost
          port: 6379

    Start Redis locally:

    docker run --name redis \
      -p 6379:6379 \
      -d redis

    7. Idempotency response object

    We need to store more than just the response.

    public record IdempotencyRecord(
            String status,
            String response
    ) {
    }

    For example Redis could contain:

    idempotency:abc-123

    with:

    {
      "status": "COMPLETED",
      "response": "{\"paymentId\":\"PAY-1001\",\"status\":\"SUCCESS\"}"
    }

    8. Redis service

    @Service
    public class IdempotencyStore {
    
        private final RedisTemplate<String, String> redisTemplate;
    
        public IdempotencyStore(RedisTemplate<String, String> redisTemplate) {
            this.redisTemplate = redisTemplate;
        }
    
        public String get(String key) {
            return redisTemplate.opsForValue().get(key);
        }
    
        public boolean createProcessingKey(
                String key,
                long ttlSeconds) {
    
            Boolean created = redisTemplate
                    .opsForValue()
                    .setIfAbsent(
                            key,
                            "PROCESSING",
                            Duration.ofSeconds(ttlSeconds)
                    );
    
            return Boolean.TRUE.equals(created);
        }
    
        public void saveResponse(
                String key,
                String response,
                long ttlSeconds) {
    
            redisTemplate.opsForValue().set(
                    key,
                    response,
                    Duration.ofSeconds(ttlSeconds)
            );
        }
    }

    The important part is:

    setIfAbsent(...)

    This is effectively:

    SET key only if it doesn't already exist

    That is important for concurrent requests.


    9. Now the important part — AOP

    @Aspect
    @Component
    public class IdempotencyAspect {
    
        private final IdempotencyStore store;
        private final ObjectMapper objectMapper;
    
        public IdempotencyAspect(
                IdempotencyStore store,
                ObjectMapper objectMapper) {
    
            this.store = store;
            this.objectMapper = objectMapper;
        }
    
        @Around("@annotation(idempotent)")
        public Object handleIdempotency(
                ProceedingJoinPoint joinPoint,
                Idempotent idempotent) throws Throwable {
    
            HttpServletRequest request =
                    ((ServletRequestAttributes)
                            RequestContextHolder
                                    .currentRequestAttributes())
                            .getRequest();
    
            String idempotencyKey =
                    request.getHeader("Idempotency-Key");
    
            // 1. Validate key
            if (idempotencyKey == null ||
                    idempotencyKey.isBlank()) {
    
                throw new IllegalArgumentException(
                        "Idempotency-Key header is required");
            }
    
            // 2. Build Redis key
            String redisKey =
                    "idempotency:" + idempotencyKey;
    
            // 3. Check existing key
            String existing =
                    store.get(redisKey);
    
            if (existing != null) {
    
                // Request already processed
                if (!existing.equals("PROCESSING")) {
    
                    return objectMapper.readValue(
                            existing,
                            PaymentResponse.class
                    );
                }
    
                // Another request is currently processing
                throw new IllegalStateException(
                        "Request is already being processed");
            }
    
            // 4. Atomically create processing key
            boolean acquired =
                    store.createProcessingKey(
                            redisKey,
                            idempotent.ttlSeconds()
                    );
    
            if (!acquired) {
    
                throw new IllegalStateException(
                        "Request is already being processed");
            }
    
            try {
    
                // 5. Execute actual controller/service method
                Object response =
                        joinPoint.proceed();
    
                // 6. Serialize response
                String responseJson =
                        objectMapper.writeValueAsString(response);
    
                // 7. Store final response
                store.saveResponse(
                        redisKey,
                        responseJson,
                        idempotent.ttlSeconds()
                );
    
                // 8. Return response
                return response;
    
            } catch (Exception e) {
    
                /*
                 * Important:
                 * Remove PROCESSING key so that a retry
                 * can attempt the operation again.
                 */
                redisTemplate.delete(redisKey);
    
                throw e;
            }
        }
    }

    There is one correction here: the aspect would need the RedisTemplate if it directly deletes the key. Better design is to put delete() inside IdempotencyStore:

    public void delete(String key) {
        redisTemplate.delete(key);
    }

    Then:

    catch (Exception e) {
        store.delete(redisKey);
        throw e;
    }

    10. Controller

    Now the controller becomes very clean.

    @RestController
    @RequestMapping("/payments")
    public class PaymentController {
    
        private final PaymentService paymentService;
    
        public PaymentController(
                PaymentService paymentService) {
    
            this.paymentService = paymentService;
        }
    
        @Idempotent
        @PostMapping
        public PaymentResponse createPayment(
                @RequestBody PaymentRequest request) {
    
            return paymentService.createPayment(request);
        }
    }

    The developer doesn't need to write:

    if (keyExists) ...

    inside every controller.

    AOP handles it.


    11. Payment service

    @Service
    public class PaymentService {
    
        public PaymentResponse createPayment(
                PaymentRequest request) {
    
            System.out.println("PROCESSING PAYMENT");
    
            // Call payment provider
            // Save payment to database
            // etc.
    
            return new PaymentResponse(
                    "PAY-1001",
                    "SUCCESS",
                    request.amount()
            );
        }
    }

    12. First request

    Client:

    POST /payments
    Idempotency-Key: abc-123
    Content-Type: application/json
    {
      "amount": 10000,
      "currency": "INR",
      "customerId": "CUST-101"
    }

    AOP:

    abc-123
       ↓
    Redis lookup
       ↓
    NOT FOUND
       ↓
    SETNX abc-123 = PROCESSING
       ↓
    joinPoint.proceed()
       ↓
    PaymentService
       ↓
    PAY-1001 SUCCESS
       ↓
    Store response

    Redis:

    idempotency:abc-123
    
    {
        "paymentId": "PAY-1001",
        "status": "SUCCESS",
        "amount": 10000
    }

    Client gets:

    {
      "paymentId": "PAY-1001",
      "status": "SUCCESS",
      "amount": 10000
    }

    13. Client retries

    Suppose response was lost:

    Backend → Payment successful
    Backend → Response
           X
        Network failure

    Client doesn't know what happened.

    It retries:

    POST /payments
    Idempotency-Key: abc-123

    AOP:

    abc-123
       ↓
    Redis
       ↓
    FOUND
       ↓
    Return stored response

    Payment service is not called.

    Client gets:

    {
      "paymentId": "PAY-1001",
      "status": "SUCCESS",
      "amount": 10000
    }

    Therefore:

    Payment created: 1
    API requests:    2

    14. What if the key is missing?

    Client:

    POST /payments

    No:

    Idempotency-Key

    AOP rejects it:

    400 Bad Request

    Response could be:

    {
      "error": "Idempotency-Key header is required"
    }

    15. What if the key is already processing?

    This situation is important.

    Imagine two requests arrive simultaneously:

    Request A ──→ abc-123
    Request B ──→ abc-123

    A:

    SETNX abc-123
         ↓
    SUCCESS
         ↓
    PROCESSING

    B:

    SETNX abc-123
         ↓
    FAIL
         ↓
    Already processing

    So B should not execute the payment.

    Depending on your API design, you might return:

    409 Conflict

    or:

    409 Request already in progress

    16. One more VERY important validation

    You shouldn't only validate:

    Idempotency-Key exists

    You should also make sure the same key isn't reused for a different request.

    For example:

    First request

    Idempotency-Key: abc-123
    {
        "amount": 10000
    }

    Then somebody sends:

    Idempotency-Key: abc-123
    {
        "amount": 50000
    }

    That's suspicious.

    The server should detect:

    Same key
         +
    Different request
         ↓
    400 Bad Request

    A common solution is to store a request fingerprint/hash:

    idempotency_key
    request_hash
    status
    response

    For example:

    abc-123
    SHA256(request)
    SUCCESS
    PAY-1001 response

    On retry:

    abc-123
    SHA256(new request)

    If hashes don't match:

    409/400
    Idempotency key reused with different request

    17. Production architecture I'd recommend

    For your Spring Boot/RAG/backend experience, understand this architecture:

                     Client
                        |
                        | Idempotency-Key
                        ↓
                  API Gateway
                        |
                        ↓
                Spring Boot API
                        |
                 @Idempotent
                        |
                        ↓
                   AOP Aspect
                        |
              ┌─────────┴─────────┐
              ↓                   ↓
           Redis                Database
              |                   |
              |              Business data
              |
              ├── PROCESSING
              |
              └── COMPLETED
                     +
                  response

    And the key production concepts are:

    Request validation

    Key exists?
    Key format valid?
    Same request hash?

    Concurrency

    SETNX / atomic operation

    Response storage

    key → status + response

    Failure handling

    PROCESSING → failure
                 ↓
           remove/recover key

    Expiration

    TTL → don't keep idempotency records forever

    Database consistency

    For something like payment, Redis idempotency alone isn't enough. The payment database/provider operation and idempotency record need a carefully designed consistency strategy. Otherwise you can still have edge cases around crashes between "payment succeeded" and "response stored."

    So the simple AOP implementation is excellent for understanding the concept, but payments require a stronger production design than just caching the controller response in Redis.

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Idempotent {
    
        /**
         * How long the response should remain cached.
         */
        long ttlSeconds() default 86400;
    }
    
    vs 
    
    
    @Aspect
    @Component
    public class IdempotencyAspect {

    They are two completely different things. This distinction is very important when learning Spring AOP.

    Think of it as:

    @Idempotent annotation
            ↓
          LABEL
            ↓
    "Which methods should have idempotency?"
            
            +
    
    @Aspect class
            ↓
         LOGIC
            ↓
    "What should happen when that method is called?"

    1. @Idempotent — custom annotation

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Idempotent {
    
        long ttlSeconds() default 86400;
    }

    This does not implement idempotency.

    It simply creates your own annotation that you can put on a method:

    @Idempotent
    @PostMapping("/payments")
    public PaymentResponse createPayment() {
        ...
    }

    Think of it like putting a sticker on the method:

    createPayment()
          ↑
       @Idempotent

    The sticker says:

    "This method needs idempotency handling."

    But the sticker itself doesn't do anything.


    2. @Aspect — actual logic

    @Aspect
    @Component
    public class IdempotencyAspect {

    This class contains the actual idempotency logic.

    For example:

    @Around("@annotation(idempotent)")
    public Object handleIdempotency(
            ProceedingJoinPoint joinPoint,
            Idempotent idempotent) {
    
        // Read Idempotency-Key
        // Check Redis
        // If exists → return previous response
        // If doesn't exist → execute method
        // Save response
    
    }

    The important part is:

    @Around("@annotation(idempotent)")

    It means:

    "Whenever a method has @Idempotent, intercept that method."


    3. How they work together

    Suppose you have:

    @Idempotent
    @PostMapping("/payments")
    public PaymentResponse createPayment(
            @RequestBody PaymentRequest request) {
    
        return paymentService.createPayment(request);
    }

    Spring sees:

                 createPayment()
                        ↑
                  @Idempotent
                        |
                        ↓
           IdempotencyAspect finds it
                        |
                        ↓
              handleIdempotency()
                        |
            ┌───────────┴───────────┐
            ↓                       ↓
        Key exists?             Key doesn't exist
            ↓                       ↓
     Return saved response    Execute method
                                    ↓
                             createPayment()
                                    ↓
                             Save response
                                    ↓
                             Return response

    4. Very simple analogy

    Imagine a security guard.

    Annotation

    @Idempotent

    is like putting a sign on a door:

    ┌───────────────────────┐
    │       PAYMENT API     │
    │                       │
    │  🔒 Idempotent        │
    └───────────────────────┘

    It tells the system:

    "This door needs special handling."

    Aspect

    @Aspect
    public class IdempotencyAspect

    is the security guard standing outside the door.

    The guard actually checks:

    Who is coming?
       ↓
    What's the key?
       ↓
    Have I seen this key?
       ↓
    YES → give previous response
    NO  → allow request through

    5. Why not put everything inside @Idempotent?

    You might wonder:

    Why don't we just put the logic inside the annotation?

    Because an annotation is primarily metadata.

    This:

    public @interface Idempotent {
        long ttlSeconds() default 86400;
    }

    basically says:

    Name: Idempotent
    Configuration: ttlSeconds = 86400

    It doesn't contain the business behavior.

    The Aspect contains the behavior.


    6. The ttlSeconds connects them

    This part is particularly useful:

    @Idempotent(ttlSeconds = 3600)

    The annotation can provide configuration to the Aspect.

    The Aspect receives it here:

    @Around("@annotation(idempotent)")
    public Object handleIdempotency(
            ProceedingJoinPoint joinPoint,
            Idempotent idempotent) {

    Then:

    idempotent.ttlSeconds()

    returns:

    3600

    So:

    @Idempotent(ttlSeconds = 3600)

    means:

    Store the idempotency result for 1 hour.

    While:

    @Idempotent

    means:

    Use the default 86400 seconds = 24 hours.


    7. Complete relationship

    // 1. Define the annotation
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Idempotent {
    
        long ttlSeconds() default 86400;
    }

    ↓

    // 2. Implement the behavior
    @Aspect
    @Component
    public class IdempotencyAspect {
    
        @Around("@annotation(idempotent)")
        public Object handle(
                ProceedingJoinPoint joinPoint,
                Idempotent idempotent) {
    
            long ttl =
                idempotent.ttlSeconds();
    
            // idempotency logic...
    
            return ...;
        }
    }

    ↓

    // 3. Developer uses it
    @Idempotent(ttlSeconds = 3600)
    @PostMapping("/payments")
    public PaymentResponse createPayment(
            @RequestBody PaymentRequest request) {
    
        return paymentService.createPayment(request);
    }

    So the relationship is:

    ┌────────────────────┐
    │   @Idempotent      │
    │                    │
    │ Metadata/config    │
    │ ttlSeconds = 3600  │
    └─────────┬──────────┘
              │
              │ Aspect detects annotation
              ↓
    ┌────────────────────┐
    │ IdempotencyAspect  │
    │                    │
    │ Actual logic       │
    │                    │
    │ Read key           │
    │ Validate            │
    │ Check Redis        │
    │ Execute method     │
    │ Store response     │
    └────────────────────┘

    One-line interview answer

    @Idempotent is a custom marker/configuration annotation that identifies which methods require idempotency, while @Aspect contains the cross-cutting logic that intercepts those methods and actually implements idempotency.