Start HereOrdered learning track

Contract-First Integration: Schema, Versioning, Compatibility, Consumer Safety

Learn AWS Application and Database - Part 016

Contract-first integration untuk API, event, queue message, workflow input, dan database projection di AWS dengan schema compatibility, versioning, validation, dan consumer safety.

6 min read1025 words
PrevNext
Lesson 1696 lesson track01–17 Start Here
#aws#application-integration#contract-first#api-gateway+7 more

Part 016 — Contract-First Integration: Schema, Versioning, Compatibility, Consumer Safety

Tujuan bagian ini: membangun cara kerja contract-first untuk API, event, queue message, workflow input, dan projection. Targetnya bukan membuat dokumentasi cantik, tetapi memastikan producer dan consumer bisa berevolusi tanpa production break, silent data corruption, atau deployment coupling yang tidak terlihat.

Service tidak berkomunikasi lewat HTTP, SQS, SNS, EventBridge, atau Step Functions saja.

Service berkomunikasi lewat contract.

Transport hanya membawa byte. Contract menentukan makna byte itu.

POST /payments bukan contract lengkap.
PaymentAccepted bukan contract lengkap.
SQS message body JSON bukan contract lengkap.
Step Functions input bukan contract lengkap.

Contract minimal harus menjawab:

apa operasi/fakta yang direpresentasikan?
field apa yang wajib?
field apa yang optional?
field apa yang stabil?
field apa yang boleh berubah?
apa satuan nilai numerik?
apa arti status?
apa semantic guarantee-nya?
apakah duplicate/replay mungkin?
apakah ordering dijamin?
bagaimana versioning dilakukan?
apa yang terjadi jika consumer lama menerima field baru?

Tanpa contract, integration hanya kebetulan cocok hari ini.


1. Contract-First vs Code-First

Code-First

Developer menulis handler/model dulu.
Schema keluar belakangan dari implementasi.
Consumer membaca contoh payload dari log atau Slack.

Masalah:

- breaking change tidak terlihat sebelum deploy,
- dokumentasi tertinggal,
- field semantic berubah tanpa review,
- consumer mengandalkan field internal,
- event menjadi dump entity database,
- test hanya memvalidasi producer, bukan consumer compatibility.

Contract-First

Contract didefinisikan sebelum implementasi producer/consumer.
Contract menjadi artifact yang direview, dites, dan diberi version.

Output contract bisa berupa:

OpenAPI untuk REST/HTTP API
GraphQL schema untuk AppSync
JSON Schema untuk event/message
AsyncAPI untuk async interface documentation
EventBridge schema registry artifact
Protocol Buffers/Avro untuk stream/binary contract
Amazon States Language input/output convention untuk workflow
SQL view/projection contract untuk read model

Contract-first bukan berarti implementasi lambat. Justru ia memotong ambiguity lebih awal.


2. Contract Bukan Hanya Shape, tapi Semantics

Shape:

{
  "paymentId": "pay_123",
  "amount": 100000,
  "status": "ACCEPTED"
}

Semantics:

paymentId adalah stable business identifier.
amount disimpan dalam minor unit, bukan decimal floating point.
currency default tidak pernah diasumsikan; harus eksplisit.
status ACCEPTED berarti command diterima, bukan settlement selesai.
Event boleh duplicate.
Event boleh datang terlambat.
Consumer harus idempotent berdasarkan eventId.

Kesalahan production sering terjadi bukan karena JSON invalid, tetapi karena semantic salah.

Contoh field status:

PENDING   menurut producer = menunggu fraud check
PENDING   menurut consumer = siap diproses pembayaran

Nama sama, makna beda, state rusak.


3. Contract Surface di AWS Application + Database

Contract surface utama:

SurfaceContract ArtifactAWS Relevance
REST/HTTP APIOpenAPI, JSON Schema, error modelAPI Gateway import/export, request validation
GraphQL APIGraphQL schemaAWS AppSync schema/resolver boundary
EventEvent envelope + JSON SchemaEventBridge Schema Registry, code bindings
Queue messageMessage envelope + payload schemaSQS/SNS message body convention
WorkflowState machine input/output schemaStep Functions execution input/result discipline
Database read modelView/table/API DTO schemaprojection compatibility, CQRS read side
External providerProvider API contract + idempotency behaviorretries, provider references, side-effect logs

4. API Contract with API Gateway

API Gateway can validate incoming REST API request bodies and parameters using models/schema. That validation is useful, but it should be treated as the first gate, not the complete business validation layer.

Example OpenAPI Contract

openapi: 3.0.3
info:
  title: Case Review API
  version: 1.0.0
paths:
  /cases/{caseId}/submit-review:
    post:
      operationId: submitCaseForReview
      parameters:
        - name: caseId
          in: path
          required: true
          schema:
            type: string
        - name: Idempotency-Key
          in: header
          required: true
          schema:
            type: string
            minLength: 16
            maxLength: 128
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SubmitCaseForReviewRequest'
      responses:
        '202':
          description: Accepted for review
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmitCaseForReviewResponse'
        '409':
          description: Conflict or invalid state transition
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
components:
  schemas:
    SubmitCaseForReviewRequest:
      type: object
      required:
        - evidenceVersion
        - submittedBy
      properties:
        evidenceVersion:
          type: integer
          minimum: 1
        submittedBy:
          type: string
        note:
          type: string
          maxLength: 2000
    SubmitCaseForReviewResponse:
      type: object
      required:
        - caseId
        - status
        - submittedAt
      properties:
        caseId:
          type: string
        status:
          type: string
          enum: [UNDER_REVIEW]
        submittedAt:
          type: string
          format: date-time
    ErrorResponse:
      type: object
      required:
        - code
        - message
        - correlationId
      properties:
        code:
          type: string
        message:
          type: string
        correlationId:
          type: string

API Compatibility Rules

Safe changes:

add optional request field
add optional response field
add new endpoint
add new enum only if clients treat unknown enum safely
increase max length if downstream supports it
make validation more permissive

Dangerous/breaking changes:

remove field
rename field
change field type
change unit: dollars -> cents, seconds -> milliseconds
make optional field required
change error code semantics
make validation stricter for existing clients
reuse enum value with new meaning
change idempotency behavior

Error Contract

Error response harus stabil.

{
  "code": "CASE_INVALID_TRANSITION",
  "message": "Case cannot be submitted from CLOSED state.",
  "correlationId": "01J...",
  "details": {
    "currentState": "CLOSED",
    "requiredState": "DRAFT"
  }
}

Jangan membuat consumer parse text message.

salah: if message contains "closed"
benar: if code == CASE_INVALID_TRANSITION

5. Event Contract

Event bukan command. Event adalah fakta yang sudah terjadi.

Event contract harus stabil karena producer tidak tahu semua consumer masa depan.

Event Envelope

{
  "specVersion": "1.0",
  "eventId": "01JZ0H9N9N7KJ2R8Y4P2B9V6ZD",
  "eventType": "CaseSubmittedForReview",
  "eventVersion": 1,
  "source": "case-service",
  "tenantId": "tenant-123",
  "aggregateType": "Case",
  "aggregateId": "case-456",
  "occurredAt": "2026-07-06T10:15:30Z",
  "correlationId": "01JZ0H7...",
  "causationId": "cmd-789",
  "data": {
    "caseId": "case-456",
    "evidenceVersion": 7,
    "submittedBy": "user-111",
    "reviewQueue": "standard"
  }
}

Envelope field invariant:

eventId         stable unique id for dedup
source          producer identity
eventType       semantic fact name
eventVersion    payload contract version
aggregateId     ordering/reconciliation anchor
occurredAt      business occurrence time, not publish time
correlationId   trace across service boundary
causationId     command/event that caused this event
tenantId        isolation and routing boundary

EventBridge Mapping

EventBridge event shape memiliki top-level fields seperti source, detail-type, dan detail. Praktik yang umum adalah mapping:

source      -> service/domain producer, e.g. com.company.case
detail-type -> event type, e.g. CaseSubmittedForReview
detail      -> envelope/data payload

Contoh PutEvents entry:

{
  "Source": "com.company.case",
  "DetailType": "CaseSubmittedForReview",
  "EventBusName": "domain-events-prod",
  "Detail": "{\"eventId\":\"01J...\",\"eventVersion\":1,\"aggregateId\":\"case-456\",\"data\":{...}}"
}

Event Schema Compatibility

Safe changes:

add optional data field
add new event type
add new event version with upcaster/parallel support
add metadata field that consumers may ignore

Breaking changes:

remove field used by consumer
rename field
change type
change semantic meaning
change eventType meaning
change event from fact to command
make old event invalid without upcaster

Event Upcaster

Upcaster mengubah event versi lama menjadi model internal baru.

CaseSubmittedForReviewV2 upcast(CaseSubmittedForReviewV1 old) {
    return new CaseSubmittedForReviewV2(
        old.eventId(),
        old.caseId(),
        old.evidenceVersion(),
        old.submittedBy(),
        ReviewPriority.NORMAL // default explicit migration rule
    );
}

Rule:

Upcaster harus deterministic.
Default value harus terdokumentasi.
Tidak boleh diam-diam mengubah meaning event lama.

6. EventBridge Schema Registry

EventBridge Schema Registry dapat menyimpan dan mengorganisasi schema event. AWS juga menyediakan schema untuk AWS service events dan mendukung code bindings untuk beberapa bahasa seperti Java, Python, TypeScript, dan Go.

Gunakan schema registry untuk:

- mendokumentasikan event contract,
- menghasilkan typed bindings,
- menemukan event shape dari event bus,
- membantu consumer development,
- mengurangi parsing manual payload JSON.

Jangan salah paham:

schema registry membantu discoverability dan typing,
tetapi tidak otomatis menjamin semantic compatibility.

Tetap butuh:

- review perubahan contract,
- compatibility test,
- consumer-driven contract test,
- versioning policy,
- deprecation window,
- replay test.

7. Queue Message Contract

SQS tidak peduli isi message body. SNS bisa filter berdasarkan message attributes. Tetapi contract tetap harus eksplisit.

Message Envelope

{
  "messageId": "01JZ...",
  "messageType": "GenerateCaseSummary",
  "messageVersion": 1,
  "producer": "case-service",
  "tenantId": "tenant-123",
  "correlationId": "01JZ...",
  "idempotencyKey": "case-456:summary:v7",
  "createdAt": "2026-07-06T10:15:30Z",
  "data": {
    "caseId": "case-456",
    "evidenceVersion": 7,
    "targetFormat": "PDF"
  }
}

Queue message bisa command-like, tidak selalu event.

Bedakan:

GenerateCaseSummary     command to worker
CaseSubmittedForReview  fact/event to interested consumers

Queue contract harus menyebut:

max processing time
visibility timeout assumption
retry behavior
DLQ policy
idempotency key
ordering requirement
message group strategy jika FIFO
payload size strategy
large payload pointer rule

8. Workflow Contract with Step Functions

Step Functions sering dipakai sebagai durable control plane. Input workflow adalah contract.

Bad Workflow Input

{
  "case": {
    "id": "case-456",
    "allFieldsFromDatabase": "..."
  }
}

Masalah:

workflow execution lama menyimpan snapshot besar,
schema internal database bocor,
perubahan entity bisa merusak running execution,
PII tersebar ke execution history tanpa sengaja.

Better Workflow Input

{
  "workflowVersion": 1,
  "tenantId": "tenant-123",
  "caseId": "case-456",
  "commandId": "cmd-789",
  "evidenceVersion": 7,
  "requestedBy": "user-111",
  "correlationId": "01JZ..."
}

Workflow dapat fetch state terbaru dari source of truth saat dibutuhkan.

Contract workflow harus menjawab:

apakah input immutable?
apakah workflow boleh berjalan lama?
apakah state machine version berubah untuk execution lama?
apakah task output disimpan penuh?
apakah retry task idempotent?
apakah callback token timeout punya compensation?

9. Database Projection Contract

Read model juga contract.

Jika service A mengekspos query dari projection, consumer tidak boleh bergantung pada struktur internal tabel.

Bad:

consumer membaca tabel projection langsung
consumer join ke database service lain
consumer bergantung pada kolom internal denormalized

Better:

projection table internal -> API/query contract eksternal

Untuk internal platform tertentu, shared analytical projection bisa diterima jika contract-nya formal:

view name stable
column meaning documented
deprecation policy exists
schema compatibility tested
ownership jelas
refresh lag documented

Projection contract harus menyebut freshness:

strong read from source of truth
read model eventually consistent, normally < 5s lag
read model rebuilt daily
field X only updated after async enrichment

Tanpa freshness contract, consumer akan salah membuat keputusan bisnis.


10. Consumer-Driven Contract Testing

Producer tidak tahu semua asumsi consumer. Maka consumer perlu menulis expectation.

Minimal test matrix:

producer emits event v1 with required fields
consumer ignores unknown optional fields
consumer handles missing optional field
consumer rejects incompatible type
consumer handles unknown enum safely
consumer handles duplicate event id
consumer handles old event version via upcaster

Example Consumer Expectation

@Test
void shouldConsumeCaseSubmittedForReviewV1WithUnknownField() {
    String event = """
    {
      "eventId": "evt-1",
      "eventType": "CaseSubmittedForReview",
      "eventVersion": 1,
      "aggregateId": "case-456",
      "occurredAt": "2026-07-06T10:15:30Z",
      "data": {
        "caseId": "case-456",
        "evidenceVersion": 7,
        "submittedBy": "user-111",
        "futureField": "must be ignored"
      }
    }
    """;

    consumer.consume(event);

    assertThat(projection.find("case-456").status()).isEqualTo("UNDER_REVIEW");
}

Contract test bukan hanya schema validation. Ia menguji behavior consumer di bawah compatibility rule.


11. Versioning Strategy

Version API by Capability, Not Every Change

Tidak semua perubahan butuh /v2.

add optional response field -> no new version
add optional request field -> no new version
change status semantics -> new version or new field
change money unit -> new version/new field required
remove field -> deprecation then major version

Prefer Additive Evolution

Daripada:

{
  "amount": 100.25
}

Lalu mengubah meaning menjadi minor unit, lebih aman:

{
  "amountDecimal": "100.25",
  "amountMinor": 10025,
  "currency": "USD"
}

Kemudian deprecate field lama dengan window eksplisit.

Enum Evolution

Enum adalah breaking-change trap.

Consumer harus punya unknown handling:

switch (status) {
    case "UNDER_REVIEW" -> handleUnderReview();
    case "APPROVED" -> handleApproved();
    case "REJECTED" -> handleRejected();
    default -> handleUnknownStatus(status);
}

Jika unknown status tidak aman, contract harus menyatakan producer tidak boleh menambah enum tanpa major version.

Event Versioning

Event version dapat ditempatkan di envelope:

{
  "eventType": "CaseSubmittedForReview",
  "eventVersion": 2,
  "data": { }
}

Pilihan strategi:

same eventType + eventVersion field
new eventType name, e.g. CaseSubmittedForReviewV2
parallel publish v1 and v2 during migration
upcaster in consumer

Tidak ada satu strategi universal. Pilih berdasarkan jumlah consumer, replay need, dan kemampuan migrasi.


12. Deployment Compatibility

Contract-first harus mendukung deployment independen.

Rule umum:

Producer baru harus kompatibel dengan consumer lama.
Consumer baru harus kompatibel dengan producer lama.
Rollback producer tidak boleh merusak consumer baru.
Replay event lama harus tetap bisa diproses.

Expand-Migrate-Contract untuk Contract

Sama seperti database schema migration:

1. Expand
   Tambah field baru optional. Producer bisa mulai mengisi, consumer lama ignore.

2. Migrate
   Consumer baru membaca field baru dan masih support field lama.

3. Observe
   Pastikan consumer lama tidak lagi aktif atau tidak membutuhkan field lama.

4. Contract
   Hapus/deprecate field lama setelah window aman.

Contoh:

v1: amount
v1.1: amount + amountMinor + currency
v1.2: consumers migrated to amountMinor
v2: amount removed or marked unsupported

13. Contract Review Checklist

Sebelum contract disetujui:

[ ] Nama operasi/event merepresentasikan intent/fact yang jelas?
[ ] Field wajib benar-benar wajib secara bisnis?
[ ] Field optional punya default semantic?
[ ] ID stabil untuk idempotency/dedup?
[ ] Satuan numerik eksplisit?
[ ] Timestamp meaning jelas: occurredAt vs publishedAt vs processedAt?
[ ] Timezone/format eksplisit?
[ ] Enum punya unknown strategy?
[ ] Error code stabil?
[ ] Retry semantics dijelaskan?
[ ] Duplicate/replay kemungkinan dijelaskan?
[ ] Ordering guarantee atau non-guarantee eksplisit?
[ ] Versioning policy jelas?
[ ] Deprecation policy jelas?
[ ] PII/sensitive field dikontrol?
[ ] Consumer compatibility test ada?
[ ] Example payload valid dan realistis?

14. Anti-Pattern

Anti-Pattern 1: Event sebagai Dump Entity Database

{
  "eventType": "CaseUpdated",
  "data": {
    "entireCaseRow": { }
  }
}

Masalah:

consumer tidak tahu field mana berubah,
internal schema bocor,
PII ikut tersebar,
event meaning kabur,
perubahan database menjadi breaking event change.

Better:

CaseSubmittedForReview
CaseAssignedToReviewer
CaseDecisionRecorded
CaseClosed

Anti-Pattern 2: Generic Event Type

EntityChanged
RecordUpdated
StatusChanged

Ini memindahkan kompleksitas ke consumer. Consumer harus membaca diff dan menebak makna.

Anti-Pattern 3: Required Field Terlalu Banyak

Required field membuat evolution sulit. Jadikan required hanya jika benar-benar invariant bisnis.

Anti-Pattern 4: Mengubah Semantic Tanpa Mengubah Contract

Contoh paling berbahaya:

status APPROVED dulu berarti supervisor approved
status APPROVED sekarang berarti system auto-approved

Field sama, meaning berubah. Consumer rusak tanpa schema error.

Anti-Pattern 5: Consumer Parse Field yang Tidak Dikontrak

Jika field tidak ada di contract, consumer tidak boleh bergantung padanya.


15. Practical Repository Layout

Contoh struktur mono-repo/platform repo:

contracts/
  apis/
    case-review-api/
      openapi.yaml
      examples/
      compatibility-tests/
  events/
    CaseSubmittedForReview/
      v1.schema.json
      v1.examples.json
      compatibility.md
    CaseDecisionRecorded/
      v1.schema.json
  messages/
    GenerateCaseSummary/
      v1.schema.json
  workflows/
    SubmitCaseReviewWorkflow/
      input.v1.schema.json
      output.v1.schema.json
  shared/
    error-response.schema.json
    envelope.schema.json

CI checks:

schema valid
examples validate against schema
breaking-change detector runs
consumer contract tests run
generated code bindings compile
OpenAPI lint passes
no undocumented required field
no forbidden sensitive fields in event payload

16. Mini Case Study: Case Review Domain

Command API

POST /cases/{caseId}/submit-review
Idempotency-Key: tenant-123:SubmitReview:cmd-789

Command means:

request transition from DRAFT to UNDER_REVIEW

It does not mean:

review completed
case approved
notification sent
all evidence validated by humans

Event

CaseSubmittedForReview

Fact means:

case entered UNDER_REVIEW state at occurredAt

Consumer expectations:

notification-service may email reviewer
analytics-service may increment submitted count
search-indexer may update searchable status
case-timeline may append timeline entry

Each consumer must dedup by:

subscriberId + eventId

Projection

GET /review-queue?status=UNDER_REVIEW

Freshness contract:

eventually consistent, normally under 10 seconds, may lag during replay/backfill

This prevents UI/business process from assuming strong consistency.


17. Ringkasan

Contract-first integration berarti:

transport is not the contract
schema shape is not enough
semantics must be explicit
compatibility must be tested
versioning must support independent deployment
consumer safety must be designed before producer rollout

API Gateway, AppSync, EventBridge Schema Registry, SQS, SNS, and Step Functions provide useful integration surfaces. But the engineering maturity comes from how you define, version, validate, test, and operate contracts across those surfaces.

A good contract lets services change independently. A bad contract turns every deployment into a distributed gamble.


References

Lesson Recap

You just completed lesson 16 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.