Start HereOrdered learning track

HTTP Contract Semantics

Learn Java API Contract Engineering, Event Contract Engineering & Schema Governance - Part 006

HTTP contract semantics untuk API engineer: method semantics, status code, headers, caching, conditional requests, optimistic concurrency, retry, dan idempotency sebagai contract surface.

20 min read3898 words
PrevNext
Lesson 0632 lesson track0106 Start Here
#java#http#api-contract#openapi+4 more

Part 006 — HTTP Contract Semantics: Status Codes, Headers, Caching, Conditional Requests, and Idempotency

1. Tujuan Part Ini

Part ini membahas HTTP sebagai contract language, bukan sekadar transport untuk JSON.

Banyak API enterprise memakai HTTP tetapi tidak memakai semantics HTTP dengan benar. Akibatnya:

  • semua outcome dibungkus 200 OK,
  • retry tidak aman,
  • client tidak tahu kapan boleh cache,
  • update race condition tidak terdeteksi,
  • error tidak dapat diproses mesin,
  • gateway/API management tidak bisa membantu,
  • observability kehilangan sinyal,
  • generated client tidak bisa memberi behavior yang benar,
  • API contract terlihat sederhana tetapi behavior-nya ambigu.

Target setelah part ini:

  1. Anda memahami HTTP method semantics sebagai bagian dari contract.
  2. Anda mampu memilih status code berdasarkan outcome, bukan kebiasaan tim.
  3. Anda mampu mendesain header sebagai control-plane contract.
  4. Anda memahami caching dan conditional request sebagai alat correctness dan performance.
  5. Anda mampu mendesain idempotency untuk create/update operation yang aman di distributed system.
  6. Anda bisa menuangkan semantics ini ke OpenAPI.

Mental model utama:

HTTP method, status code, header, cache directive, dan precondition bukan detail protokol rendah. Mereka adalah vocabulary kontrak antara provider, consumer, gateway, proxy, cache, load balancer, observability platform, dan manusia yang mengoperasikan sistem.


2. Kaufman Framing: Sub-Skill HTTP Contract

Agar efektif, pecah skill ini menjadi beberapa sub-skill:

Sub-skillOutput Praktis
Method semanticsBisa memilih GET/POST/PUT/PATCH/DELETE dengan benar
Status code taxonomyBisa memetakan outcome ke response code
Header contractBisa mendesain correlation, idempotency, cache, auth, concurrency
Conditional requestBisa mencegah lost update
Caching contractBisa membedakan private/public/no-store/revalidation
Retry semanticsBisa menjelaskan kapan consumer boleh retry
OpenAPI encodingBisa menulis semantics ke spec
Failure modelingBisa membedakan conflict, validation error, dependency failure, timeout

Latihan minimal:

  1. Pilih satu mutating API.
  2. Tentukan method dan status code untuk semua outcome.
  3. Tambahkan idempotency dan correlation header.
  4. Tambahkan optimistic concurrency jika resource bisa diupdate bersamaan.
  5. Tambahkan OpenAPI contract.
  6. Review apakah consumer bisa membangun retry logic tanpa bertanya ke provider.

3. HTTP Bukan Sekadar “REST Transport”

Bentuk anti-pattern:

POST /api
Content-Type: application/json

{
  "action": "createCase",
  "payload": {
    "subjectId": "ACC-123"
  }
}

Lalu semua response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "success": false,
  "errorCode": "CASE_ALREADY_EXISTS"
}

Ini memakai HTTP sebagai tunnel. Banyak semantics hilang:

  • method tidak menunjukkan safety/idempotency,
  • path tidak menunjukkan resource,
  • status code tidak menunjukkan outcome,
  • cache tidak bisa bekerja,
  • gateway tidak bisa classify error,
  • monitoring tidak bisa group status dengan benar,
  • client harus memahami custom envelope untuk semua hal.

Desain yang lebih baik:

POST /cases
Idempotency-Key: 20260629-create-case-abc123
Content-Type: application/json
Accept: application/json

{
  "subject": {
    "type": "ACCOUNT",
    "id": "ACC-123"
  },
  "allegationType": "FRAUD"
}

Success:

HTTP/1.1 201 Created
Location: /cases/CASE-0000001001
Content-Type: application/json

{
  "id": "CASE-0000001001",
  "status": "OPEN"
}

Conflict:

HTTP/1.1 409 Conflict
Content-Type: application/json

{
  "code": "DUPLICATE_ACTIVE_CASE",
  "message": "An active case already exists for this subject.",
  "traceId": "4bf92f3577b34da6a3ce929d0e0e4736"
}

Transport semantics sekarang membantu contract.


4. Method Semantics: Safety dan Idempotency

HTTP methods membawa semantics.

MethodSafeIdempotentCommon UseContract Note
GETYesYesRetrieve representationTidak boleh mengubah state yang meaningful
HEADYesYesRetrieve metadata onlySama seperti GET tanpa body
POSTNoNo by defaultCreate subordinate resource, command, processingPerlu idempotency design untuk retry aman
PUTNoYesReplace resource at known URIIdempotent jika semantics dijaga
PATCHNoNot necessarilyPartial updateHarus mendefinisikan patch format dan concurrency strategy
DELETENoYesDelete/remove resourceIdempotency outcome perlu jelas
OPTIONSYesYesCapabilitiesJarang digunakan langsung oleh business API

4.1 Safe Method

Safe berarti client tidak meminta state-changing behavior. Provider masih boleh melakukan logging, metrics, cache warming, atau audit read, tetapi tidak boleh mengubah business state sebagai efek utama.

Buruk:

GET /cases/{caseId}/close

Masalah:

  • crawler bisa memanggil,
  • cache/proxy bisa mengulang,
  • prefetcher bisa memicu state change,
  • semantics salah.

Lebih benar:

POST /cases/{caseId}/closures

4.2 Idempotent Method

Idempotent berarti beberapa request identik memiliki efek akhir yang sama seperti satu request.

Contoh PUT:

PUT /cases/CASE-0001/priority
Content-Type: application/json

{
  "priority": "HIGH"
}

Mengirim request yang sama berkali-kali tetap menghasilkan priority HIGH.

Namun idempotent bukan berarti response harus selalu sama. Request pertama bisa 200, request berikutnya juga 200, atau dalam beberapa desain 204. Yang penting efek akhirnya tidak berlipat.

4.3 POST Tidak Otomatis Buruk

POST sering diperlukan untuk:

  • create resource dengan server-generated ID,
  • menjalankan command,
  • submit form/process,
  • search kompleks yang terlalu besar untuk query string,
  • asynchronous processing.

Tetapi POST mutating yang penting harus punya idempotency strategy.


5. Resource vs Command: Memilih Path dan Method

Tidak semua operation natural sebagai CRUD.

Contoh domain regulatory case management:

Business IntentGood HTTP ShapeReason
Create casePOST /casesServer creates subordinate resource
Get caseGET /cases/{caseId}Retrieve representation
Replace case metadataPUT /cases/{caseId}/metadataKnown resource replacement
Patch narrativePATCH /cases/{caseId}Partial update
Assign officerPOST /cases/{caseId}/assignmentsCreates assignment record/action
Place holdPOST /cases/{caseId}/holdsHold is auditable sub-resource
Release holdPOST /cases/{caseId}/hold-releasesAuditable command/result
Close casePOST /cases/{caseId}/closuresBusiness command with policy
Delete draft caseDELETE /cases/{caseId}Resource removal if allowed

Why not POST /cases/{caseId}/close?

It can be acceptable, but creating a sub-resource like /closures is often more auditable. It gives a place for:

  • closure reason,
  • actor,
  • approval,
  • timestamp,
  • policy version,
  • decision evidence.

Dalam regulated systems, state transition sering lebih baik dimodelkan sebagai auditable command/resource daripada sekadar verb endpoint.


6. Status Code Taxonomy: Outcome Contract

Status code harus menjawab:

  1. Apakah request diterima secara syntax?
  2. Apakah caller authenticated?
  3. Apakah caller authorized?
  4. Apakah target resource ada?
  5. Apakah state target memungkinkan operation?
  6. Apakah request sudah diproses?
  7. Apakah consumer boleh retry?
  8. Apakah error bersifat client-side atau provider-side?

6.1 Success Codes

CodeUseContract Meaning
200 OKSuccessful request with response bodyOutcome selesai dan representation dikembalikan
201 CreatedNew resource createdResource URI sebaiknya tersedia via Location
202 AcceptedRequest diterima untuk async processingBelum selesai; perlu status tracking
204 No ContentSuccess without bodyJangan kirim body
206 Partial ContentRange responseRelevan untuk byte range, bukan pagination biasa

6.2 Client Error Codes

CodeUseContract Meaning
400 Bad RequestSyntax/schema malformed or general bad requestConsumer perlu memperbaiki request
401 UnauthorizedAuthentication missing/invalidButuh credential/token valid
403 ForbiddenAuthenticated but not allowedCredential valid tapi permission kurang
404 Not FoundResource not found or hiddenJangan leak existence jika security menuntut
405 Method Not AllowedMethod tidak didukung untuk resourceSertakan Allow jika relevan
409 ConflictConflict dengan current stateConsumer mungkin perlu refresh state
412 Precondition FailedConditional request gagalETag/version tidak cocok
415 Unsupported Media TypeContent-Type tidak didukungConsumer salah media type
422 Unprocessable Content/EntitySemantically invalid payloadTim harus konsisten jika memakai ini
428 Precondition RequiredProvider mensyaratkan conditional requestMencegah lost update
429 Too Many RequestsRate limitSertakan retry guidance jika memungkinkan

6.3 Server Error Codes

CodeUseContract Meaning
500 Internal Server ErrorUnhandled provider failureConsumer mungkin retry tergantung operation
502 Bad GatewayUpstream invalid responseBiasanya gateway/proxy
503 Service UnavailableTemporary unavailableRetry mungkin aman jika idempotent
504 Gateway TimeoutUpstream timeoutOutcome mungkin unknown untuk mutating operation

Critical point:

Untuk mutating request, timeout/5xx tidak selalu berarti operation gagal. Bisa jadi operation sudah committed tetapi response hilang.

Karena itu idempotency penting.


7. 400 vs 409 vs 422: Jangan Campur Semuanya

Perdebatan umum:

  • pakai 400 untuk semua invalid request,
  • pakai 422 untuk semantic validation,
  • pakai 409 untuk business state conflict.

Yang penting bukan memenangkan debat, tapi membuat taxonomy konsisten.

Rekomendasi enterprise:

SituationRecommended CodeExample
JSON malformed400Body tidak valid JSON
Required field missing400 or 422, pilih konsistensubjectId hilang
Field format invalid400 or 422, pilih konsistencurrency bukan ISO-style code
Business rule conflict with current state409Case sudah closed
Optimistic concurrency mismatch412If-Match tidak cocok
Unsupported media type415Content-Type: text/plain
Auth missing401Token tidak ada
Permission denied403Scope tidak cukup

Jangan lakukan ini:

{
  "status": 400,
  "code": "CASE_ALREADY_CLOSED"
}

CASE_ALREADY_CLOSED biasanya state conflict, bukan malformed request.

Lebih baik:

HTTP/1.1 409 Conflict
Content-Type: application/json

{
  "code": "CASE_ALREADY_CLOSED",
  "message": "Case cannot be assigned because it is already closed.",
  "traceId": "..."
}

8. 202 Accepted: Jangan Berbohong Tentang Completion

Gunakan 202 Accepted ketika request diterima tetapi processing belum selesai.

Contoh:

POST /case-import-jobs
Content-Type: application/json

{
  "sourceFileId": "FILE-123"
}

Response:

HTTP/1.1 202 Accepted
Location: /case-import-jobs/JOB-789
Retry-After: 10
Content-Type: application/json

{
  "jobId": "JOB-789",
  "status": "QUEUED"
}

Kontrak 202 harus menyediakan:

  1. tracking resource,
  2. status lifecycle,
  3. retry/polling guidance,
  4. eventual success/failure representation,
  5. idempotency behavior jika submit diulang,
  6. retention policy untuk job result.

Buruk:

HTTP/1.1 202 Accepted

{
  "message": "processing"
}

Consumer tidak tahu harus apa.


9. Headers as Control Plane

Header adalah bagian dari contract. Jangan hanya fokus pada JSON body.

HeaderDirectionContract Role
Content-TypeRequest/ResponseMedia type payload
AcceptRequestResponse media type negotiation
AuthorizationRequestCredential carrier
Idempotency-KeyRequestRetry-safe mutating request
ETagResponseRepresentation version validator
If-MatchRequestOptimistic concurrency precondition
If-None-MatchRequestCache validation / conditional create patterns
Cache-ControlResponse/RequestCaching policy
LocationResponseURI of created/accepted resource
Retry-AfterResponseBackoff guidance
RateLimit-*ResponseRate-limit visibility if adopted
traceparentRequest/ResponseDistributed tracing context
X-Correlation-IdRequest/ResponseBusiness/application correlation

9.1 Header Naming

Prefer standardized headers when available. Gunakan X-* hanya untuk private/internal conventions. Banyak organisasi tetap memakai X-Correlation-Id; itu boleh jika distandarkan internal, tetapi jangan menganggapnya universal standard.

9.2 Header in OpenAPI

components:
  parameters:
    IdempotencyKeyHeader:
      name: Idempotency-Key
      in: header
      required: true
      schema:
        type: string
        minLength: 1
        maxLength: 128
      description: Unique client-generated key for safely retrying this operation.
    IfMatchHeader:
      name: If-Match
      in: header
      required: true
      schema:
        type: string
      description: ETag value obtained from the latest representation.
  headers:
    ETag:
      schema:
        type: string
      description: Entity tag representing the current resource representation.
    Location:
      schema:
        type: string
      description: URI of the created or accepted resource.

10. Content Negotiation Contract

Content-Type menjelaskan media type request body.

Content-Type: application/json

Accept menjelaskan response media type yang diinginkan.

Accept: application/json

OpenAPI:

requestBody:
  content:
    application/json:
      schema:
        $ref: '#/components/schemas/CreateCaseRequest'
responses:
  '201':
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/Case'

Jika API mendukung versi via media type:

Accept: application/vnd.example.case.v1+json

Maka versioning menjadi bagian content negotiation contract. Ini bisa powerful, tetapi lebih kompleks untuk tooling, docs, client generation, gateway routing, dan support.

Jika tidak perlu, jangan pilih strategi versioning yang paling kompleks hanya agar terlihat sophisticated.


11. Caching Contract

Caching bukan hanya performance. Caching adalah contract tentang reuse response.

Basic response:

HTTP/1.1 200 OK
Cache-Control: private, max-age=60
ETag: "case-123-v7"
Content-Type: application/json

{
  "id": "CASE-123",
  "status": "OPEN"
}

Meaning:

  • response boleh disimpan private cache,
  • fresh selama 60 detik,
  • ETag bisa dipakai revalidation.

11.1 Common Cache-Control Directives

DirectiveMeaning
no-storeJangan simpan response di cache
no-cacheBoleh simpan, tetapi harus revalidate sebelum reuse
privateHanya private cache, bukan shared cache
publicShared cache boleh menyimpan jika syarat terpenuhi
max-age=NFresh selama N detik
s-maxage=NFreshness khusus shared cache
must-revalidateJangan gunakan stale response setelah expired tanpa revalidation

Untuk data sensitif:

Cache-Control: no-store

Untuk user-specific data:

Cache-Control: private, no-cache

Untuk reference data yang jarang berubah:

Cache-Control: public, max-age=3600
ETag: "refdata-v42"

11.2 Caching Mistakes

  1. Tidak mengirim Cache-Control sama sekali untuk API sensitif.
  2. Mengirim public untuk user-specific/PII data.
  3. Menggunakan no-cache padahal maksudnya no-store.
  4. Mengubah response tanpa mengubah validator.
  5. Menggunakan cache untuk command response yang tidak boleh reused.
  6. Tidak mendokumentasikan caching di OpenAPI.

12. ETag and Conditional GET

ETag adalah validator untuk representation.

Request pertama:

GET /cases/CASE-123

Response:

HTTP/1.1 200 OK
ETag: "case-123-v7"
Cache-Control: private, no-cache
Content-Type: application/json

{
  "id": "CASE-123",
  "status": "OPEN"
}

Request berikutnya:

GET /cases/CASE-123
If-None-Match: "case-123-v7"

Jika tidak berubah:

HTTP/1.1 304 Not Modified
ETag: "case-123-v7"

Manfaat:

  • hemat bandwidth,
  • memperjelas representation version,
  • mendukung cache revalidation,
  • menjadi foundation untuk optimistic concurrency.

ETag harus mewakili representation yang relevan. Jangan gunakan timestamp kasar yang bisa collision jika update cepat.


13. Optimistic Concurrency with If-Match

Lost update problem:

Without If-Match, B might overwrite A's update.

OpenAPI:

/cases/{caseId}:
  patch:
    operationId: updateCase
    parameters:
      - name: caseId
        in: path
        required: true
        schema:
          type: string
      - name: If-Match
        in: header
        required: true
        schema:
          type: string
        description: ETag from the latest representation.
    requestBody:
      required: true
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PatchCaseRequest'
    responses:
      '200':
        description: Case updated.
        headers:
          ETag:
            schema:
              type: string
      '412':
        description: Precondition failed because the resource has changed.

13.1 409 vs 412

Use 412 Precondition Failed when HTTP precondition header fails.

Use 409 Conflict when request conflicts with business state.

Examples:

SituationCode
If-Match ETag outdated412
Assigning closed case409
Closing case that has unresolved mandatory task409
Updating without required If-Match where policy demands it428

14. PATCH Semantics: Define the Patch Format

PATCH is not self-explanatory. You must define patch document semantics.

Common options:

  1. JSON Merge Patch.
  2. JSON Patch.
  3. Custom partial update DTO.

14.1 Custom Partial Update DTO

PatchCaseRequest:
  type: object
  additionalProperties: false
  properties:
    priority:
      type: string
      enum:
        - LOW
        - MEDIUM
        - HIGH
    narrative:
      type:
        - string
        - 'null'

But now absent vs null matters:

  • absent means do not change,
  • null may mean clear field,
  • string means set field.

Java DTO must preserve this distinction if null is meaningful.

14.2 JSON Patch

PATCH /cases/CASE-123
Content-Type: application/json-patch+json
If-Match: "v7"

[
  { "op": "replace", "path": "/priority", "value": "HIGH" }
]

Powerful, but exposes document paths and is harder to validate semantically.

14.3 JSON Merge Patch

PATCH /cases/CASE-123
Content-Type: application/merge-patch+json
If-Match: "v7"

{
  "priority": "HIGH",
  "narrative": null
}

Simple, but null-as-delete semantics must be understood.

Rule:

Never expose PATCH without specifying the patch media type and null/absent semantics.


15. Idempotency-Key Pattern

Distributed systems fail in uncertain ways.

Scenario:

Without idempotency, retry can create duplicate payment/case/order.

15.1 Contract Rules

For an idempotency key to be a real contract:

  1. Client generates unique key per logical operation.
  2. Server stores key with request fingerprint and result.
  3. Retry with same key and same payload returns same or equivalent result.
  4. Retry with same key but different payload is rejected.
  5. Key has retention window.
  6. Behavior under concurrent duplicate request is defined.
  7. Applicable methods/operations are documented.
  8. Error and timeout semantics are documented.

15.2 OpenAPI

components:
  parameters:
    IdempotencyKeyHeader:
      name: Idempotency-Key
      in: header
      required: true
      schema:
        type: string
        minLength: 1
        maxLength: 128
      description: |
        Unique client-generated key for safely retrying this operation.
        The same key with the same request payload returns the original outcome
        within the idempotency retention window. The same key with a different
        request payload is rejected.

Operation:

/payments:
  post:
    operationId: createPayment
    parameters:
      - $ref: '#/components/parameters/IdempotencyKeyHeader'
    responses:
      '201':
        description: Payment created.
      '409':
        description: Request conflicts with an existing operation or resource state.
      '422':
        description: Idempotency key reused with a different payload.

Some organizations prefer 409 for key collision with different payload; others use 422. Choose and standardize.

15.3 Storage Model

Conceptual table:

idempotency_record
- key
- operation_name
- request_fingerprint
- status: PROCESSING | SUCCEEDED | FAILED_RETRYABLE | FAILED_FINAL
- response_status
- response_body
- resource_id
- created_at
- expires_at

Important invariants:

  • unique key scoped by tenant/client/operation,
  • request fingerprint prevents accidental reuse,
  • concurrent first requests must not both execute,
  • response replay should not expose stale sensitive data beyond retention policy,
  • records should expire safely.

15.4 Common Mistakes

  1. Key accepted but not persisted transactionally.
  2. Key scoped globally, causing collision across clients.
  3. Same key with different payload silently returns old result.
  4. Server returns 500 for duplicate in-progress request without guidance.
  5. Retention window undocumented.
  6. Idempotency implemented only at controller layer while side effect happens downstream.
  7. Idempotency key not included in logs/traces.
  8. Idempotency key used as business ID without clear semantics.

16. Retry Semantics

Consumers need to know when retry is safe.

SituationRetry?Notes
GET failed with 503Usually yesSafe/idempotent
POST create without idempotency key timed outDangerousOutcome unknown
POST create with idempotency key timed outYesRetry same key/payload
PATCH with If-Match failed 412No blind retryRefresh state first
429 with Retry-AfterRetry after delayRespect throttling
400 validation errorNoFix request
409 business conflictUsually no blind retryResolve state/business issue
500 after idempotent PUTUsually yes with backoffVerify semantics
504 after mutating requestUnknownIdempotency required

OpenAPI can document retry hints using headers and descriptions:

responses:
  '429':
    description: Rate limit exceeded.
    headers:
      Retry-After:
        schema:
          type: integer
        description: Seconds to wait before retrying.

But retry policy often lives in SDK/client docs as well.


17. Rate Limit Contract

Rate limiting is not only infrastructure. It affects consumer behavior.

A useful 429 response includes:

HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json

{
  "code": "RATE_LIMIT_EXCEEDED",
  "message": "Request rate exceeded. Retry after 60 seconds.",
  "traceId": "..."
}

Contract questions:

  1. Is limit per API key, tenant, user, IP, operation, or region?
  2. Is it fixed window, sliding window, token bucket, or adaptive?
  3. Is burst allowed?
  4. Is Retry-After always present?
  5. Are rate-limit headers exposed?
  6. Are different scopes documented?
  7. Are partner tiers different?

Even if exact algorithms are internal, consumer-facing behavior must be clear enough for safe integration.


18. Correlation, Trace, and Audit Headers

Common distinction:

ConceptPurpose
Trace IDDistributed tracing across services/spans
Correlation IDBusiness/application request correlation
Request IDUnique ID for a specific HTTP request
Idempotency KeyRetry identity for logical operation
Audit IDCompliance/audit trail reference

Do not collapse all into one field.

Example:

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
X-Correlation-Id: CASE-INTAKE-20260629-ABC
Idempotency-Key: 20260629-create-case-abc123

OpenAPI:

parameters:
  - name: traceparent
    in: header
    required: false
    schema:
      type: string
    description: W3C trace context header when distributed tracing is propagated.
  - name: X-Correlation-Id
    in: header
    required: false
    schema:
      type: string
    description: Business correlation identifier supplied by caller.

19. Modeling HTTP Semantics in OpenAPI

A contract engineer should encode HTTP semantics explicitly.

/cases/{caseId}:
  get:
    operationId: getCase
    parameters:
      - name: caseId
        in: path
        required: true
        schema:
          type: string
      - name: If-None-Match
        in: header
        required: false
        schema:
          type: string
    responses:
      '200':
        description: Case representation.
        headers:
          ETag:
            schema:
              type: string
          Cache-Control:
            schema:
              type: string
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Case'
      '304':
        description: Case representation has not changed.
      '404':
        $ref: '#/components/responses/NotFound'
  patch:
    operationId: patchCase
    parameters:
      - name: caseId
        in: path
        required: true
        schema:
          type: string
      - name: If-Match
        in: header
        required: true
        schema:
          type: string
    requestBody:
      required: true
      content:
        application/merge-patch+json:
          schema:
            $ref: '#/components/schemas/PatchCaseRequest'
    responses:
      '200':
        description: Case updated.
        headers:
          ETag:
            schema:
              type: string
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Case'
      '409':
        $ref: '#/components/responses/Conflict'
      '412':
        $ref: '#/components/responses/PreconditionFailed'
      '428':
        $ref: '#/components/responses/PreconditionRequired'

This is dramatically more useful than:

responses:
  '200':
    description: OK

20. Java Implementation Implications

Walaupun part ini fokus contract, kita perlu memahami efeknya ke Java.

20.1 Spring Controller Example

@RestController
@RequestMapping("/cases")
class CaseController {

    @PostMapping
    ResponseEntity<CaseResponse> createCase(
            @RequestHeader("Idempotency-Key") String idempotencyKey,
            @RequestHeader(value = "X-Correlation-Id", required = false) String correlationId,
            @Valid @RequestBody CreateCaseRequest request) {

        CreatedCase created = caseApplicationService.createCase(
                idempotencyKey,
                correlationId,
                request
        );

        URI location = URI.create("/cases/" + created.id());

        return ResponseEntity
                .created(location)
                .eTag(created.etag())
                .body(CaseResponse.from(created));
    }
}

20.2 Conditional Update

@PatchMapping("/{caseId}")
ResponseEntity<CaseResponse> patchCase(
        @PathVariable String caseId,
        @RequestHeader("If-Match") String ifMatch,
        @RequestBody PatchCaseRequest patch) {

    UpdatedCase updated = caseApplicationService.patchCase(caseId, ifMatch, patch);

    return ResponseEntity
            .ok()
            .eTag(updated.etag())
            .body(CaseResponse.from(updated));
}

20.3 Exception Mapping

@RestControllerAdvice
class ApiExceptionHandler {

    @ExceptionHandler(PreconditionFailedException.class)
    ResponseEntity<ErrorResponse> preconditionFailed(PreconditionFailedException ex) {
        return ResponseEntity
                .status(HttpStatus.PRECONDITION_FAILED)
                .body(ErrorResponse.of("PRECONDITION_FAILED", ex.getMessage()));
    }

    @ExceptionHandler(StateConflictException.class)
    ResponseEntity<ErrorResponse> conflict(StateConflictException ex) {
        return ResponseEntity
                .status(HttpStatus.CONFLICT)
                .body(ErrorResponse.of(ex.code(), ex.getMessage()));
    }
}

Implementation harus mengikuti contract, bukan sebaliknya.


21. HTTP Contract Decision Matrix

Design QuestionPreferred DecisionWhen to Deviate
Create with server IDPOST /resources + 201 + LocationUse PUT if client controls ID
Create with retry safetyRequire Idempotency-KeyLow-risk non-critical operation
Full replacementPUT /resources/{id}If replacement semantics unavailable
Partial updatePATCH with explicit media typeUse sub-resource command for complex business action
Concurrent update protectionETag + If-MatchSingle-writer resources or append-only actions
Async processing202 + status resourceIf completion is immediate and committed
Business state conflict409If precondition header failed, use 412
Validation failure400 or 422 consistentlyDo not mix randomly
Sensitive data responseCache-Control: no-storeReference/public data only
Rate limit429 + Retry-AfterIf policy forbids exposing retry timing

22. Anti-Patterns

22.1 GET That Mutates Business State

GET /cases/CASE-1/approve

This violates safe method expectation.

22.2 POST Everything

POST /getCase
POST /updateCase
POST /deleteCase

This hides semantics from infrastructure and humans.

22.3 200 for All Errors

HTTP/1.1 200 OK

{
  "success": false
}

This breaks monitoring, retries, gateway policies, and client expectations.

22.4 Missing Idempotency on Critical Create

Payment, order, case creation, account opening, payout, transfer, and regulatory decision submission should not rely on blind retry.

22.5 ETag That Does Not Track Real Representation Change

If ETag stays same while meaningful representation changes, consumer cache/concurrency behavior becomes wrong.

22.6 PATCH Without Patch Semantics

PATCH /cases/CASE-1

{
  "priority": null
}

Does null mean clear, ignore, invalid, or unknown? Contract must say.

22.7 Cache-Control Ignored for Sensitive Data

Leaving caching implicit on API with confidential data is a governance smell.


23. Production Failure Modes

23.1 Duplicate Side Effects

Cause:

  • client retries POST after timeout,
  • provider lacks idempotency,
  • downstream side effect committed twice.

Prevention:

  • idempotency key,
  • transactional idempotency record,
  • deduplication at downstream boundary,
  • idempotent event publication.

23.2 Lost Update

Cause:

  • two clients read same state,
  • both update,
  • second overwrites first.

Prevention:

  • ETag,
  • If-Match,
  • version column,
  • 412 response,
  • merge UI/workflow.

23.3 Stale Authorization/Data Exposure via Cache

Cause:

  • private data cached by shared proxy,
  • missing/no wrong Cache-Control.

Prevention:

  • Cache-Control: no-store for highly sensitive data,
  • private for user-specific cacheable data,
  • API gateway policy,
  • cache testing.

23.4 Consumer Retry Storm

Cause:

  • provider returns 503,
  • no Retry-After,
  • clients retry immediately,
  • outage amplifies.

Prevention:

  • backoff policy,
  • Retry-After,
  • circuit breaker,
  • rate limit,
  • idempotent retry.

23.5 Incorrect Semantic Monitoring

Cause:

  • all errors returned as 200,
  • monitoring sees success,
  • incident detected late.

Prevention:

  • proper status codes,
  • error code metrics,
  • contract-level SLO.

24. Testing HTTP Contract Semantics

Contract tests should verify more than schema.

24.1 Test Status Mapping

@Test
void shouldReturn409WhenAssigningClosedCase() {
    // arrange: closed case
    // act: POST /cases/{caseId}/assignments
    // assert: 409 + error code CASE_ALREADY_CLOSED
}

24.2 Test Idempotency

@Test
void shouldReturnSameResultWhenCreateCaseRetriedWithSameIdempotencyKey() {
    String key = UUID.randomUUID().toString();

    var first = client.createCase(key, request);
    var second = client.createCase(key, request);

    assertThat(first.statusCode()).isEqualTo(201);
    assertThat(second.body().id()).isEqualTo(first.body().id());
}

24.3 Test Idempotency Misuse

@Test
void shouldRejectSameIdempotencyKeyWithDifferentPayload() {
    String key = UUID.randomUUID().toString();

    client.createCase(key, requestA);
    var response = client.createCase(key, requestB);

    assertThat(response.statusCode()).isIn(409, 422);
}

24.4 Test Conditional Update

@Test
void shouldReturn412WhenIfMatchIsStale() {
    var caseView = client.getCase("CASE-1");
    String staleEtag = caseView.etag();

    client.patchCase("CASE-1", staleEtag, patchA);
    var response = client.patchCase("CASE-1", staleEtag, patchB);

    assertThat(response.statusCode()).isEqualTo(412);
}

24.5 Test Cache Headers

@Test
void sensitiveCaseDetailShouldNotBeStoredInSharedCache() {
    var response = client.getCase("CASE-1");

    assertThat(response.header("Cache-Control")).contains("no-store");
}

25. OpenAPI Review Checklist for HTTP Semantics

Method

  • Apakah GET bebas business side effect?
  • Apakah PUT benar-benar replacement/idempotent?
  • Apakah PATCH punya media type dan null/absent semantics?
  • Apakah POST mutating punya idempotency strategy?

Status Code

  • Apakah success code membedakan 200/201/202/204?
  • Apakah validation, auth, conflict, precondition, rate limit jelas?
  • Apakah 5xx tidak dipakai untuk business error?
  • Apakah timeout/unknown outcome dijelaskan untuk mutating operation?

Headers

  • Apakah Content-Type dan Accept jelas?
  • Apakah Location dikirim untuk created/accepted resource?
  • Apakah ETag dikirim untuk mutable resource representation?
  • Apakah If-Match wajib untuk update yang rawan lost update?
  • Apakah Retry-After ada untuk 429/503 jika relevan?
  • Apakah correlation/trace headers distandarkan?

Caching

  • Apakah sensitive data memakai no-store?
  • Apakah cacheable reference data punya max-age/ETag?
  • Apakah 304 flow didokumentasikan jika conditional GET didukung?

Idempotency

  • Apakah key scope jelas?
  • Apakah retention window dijelaskan?
  • Apakah duplicate same payload behavior jelas?
  • Apakah duplicate different payload behavior jelas?
  • Apakah concurrent duplicate behavior jelas?

26. Latihan 20 Jam: HTTP Contract Semantics

Drill 1 — Status Code Refactoring

Ambil API existing yang memakai 200 untuk semua hal.

Buat mapping baru:

Old Body CodeNew HTTP CodeNew Error Code
VALIDATION_FAILED400/422VALIDATION_FAILED
NOT_ALLOWED403FORBIDDEN
CASE_CLOSED409CASE_ALREADY_CLOSED
VERSION_MISMATCH412PRECONDITION_FAILED

Drill 2 — Idempotent Create

Desain create API untuk resource penting:

  • request,
  • success response,
  • duplicate same-key behavior,
  • duplicate different-payload behavior,
  • storage model,
  • OpenAPI header,
  • tests.

Drill 3 — Conditional Update

Ambil mutable resource.

Tambahkan:

  • ETag pada GET,
  • If-Match pada PATCH/PUT,
  • 412 response,
  • 428 jika header wajib tapi hilang,
  • test lost update.

Drill 4 — Cache Classification

Klasifikasikan endpoint:

EndpointCache Policy
Current user profileprivate/no-cache or no-store
Case detail with PIIno-store
Reference country listpublic max-age + ETag
Audit log exportno-store
Static product catalogpublic max-age

27. Ringkasan

HTTP contract semantics adalah salah satu pembeda engineer biasa dan engineer senior.

Engineer biasa melihat:

“Endpoint menerima JSON dan mengembalikan JSON.”

Engineer senior melihat:

“Method, status code, header, cache policy, precondition, retry rule, idempotency key, dan response body bersama-sama membentuk contract yang menentukan correctness consumer dan behavior sistem saat gagal.”

Prinsip penting:

  1. Jangan jadikan HTTP tunnel bodoh.
  2. Gunakan method semantics dengan benar.
  3. Pilih status code berdasarkan outcome.
  4. Treat header as contract.
  5. Gunakan caching secara eksplisit, terutama untuk sensitive data.
  6. Gunakan ETag dan If-Match untuk mencegah lost update.
  7. Gunakan Idempotency-Key untuk mutating POST yang perlu retry aman.
  8. Dokumentasikan semua ini di OpenAPI.
  9. Uji semantics, bukan hanya schema.

Jika contract tidak menjelaskan retry, cache, concurrency, dan failure semantics, consumer akan mengarang sendiri. Dalam distributed system, asumsi liar adalah sumber incident.


28. Referensi

Lesson Recap

You just completed lesson 06 in start here. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.