Series MapLesson 17 / 60
Build CoreOrdered learning track

Learn Enterprise Cpq Oms Glassfish Camunda8 Part 017 Api Versioning And Backward Compatibility

18 min read3526 words
PrevNext
Lesson 1760 lesson track1233 Build Core

title: Build From Scratch: Enterprise Java Microservices CPQ & Order Management Platform - Part 017 description: Mendesain API versioning dan backward compatibility untuk enterprise CPQ/OMS: semantic compatibility, OpenAPI diff, schema evolution, event compatibility, workflow compatibility, deprecation, rollout, dan Java implementation boundary. series: learn-enterprise-cpq-oms-glassfish-camunda8 seriesTitle: Build From Scratch: Enterprise Java Microservices CPQ & Order Management Platform order: 17 partTitle: API Versioning and Backward Compatibility tags:

  • java
  • microservices
  • cpq
  • oms
  • openapi
  • json-schema
  • api-versioning
  • backward-compatibility
  • contract-testing
  • enterprise-architecture date: 2026-07-02

Part 017 — API Versioning and Backward Compatibility

Di Part 013 sampai Part 016 kita membangun pondasi API-first dan schema-first:

  • OpenAPI sebagai kontrak HTTP API,
  • JSON Schema sebagai kontrak struktur payload,
  • error model,
  • pagination,
  • filtering,
  • idempotency,
  • optimistic concurrency,
  • dan domain schema untuk configuration, price, quote, order, event, audit, serta workflow variable.

Sekarang kita masuk ke masalah yang sering diremehkan:

Bagaimana mengubah API tanpa menghancurkan consumer, order yang sedang berjalan, workflow yang masih hidup, event replay, dan integrasi enterprise yang lambat berubah?

Versioning bukan solusi ajaib. Versioning adalah biaya organisasi.

Kalau setiap perubahan kecil langsung dibuat /v2, sistem akan cepat menjadi museum API mati. Sebaliknya, kalau semua perubahan dipaksakan masuk /v1 tanpa compatibility discipline, consumer akan pecah diam-diam.

Target kita bukan sekadar punya versi.

Target kita adalah punya evolution model.


1. Mental Model: Compatibility Lebih Penting Daripada Version Number

Nomor versi tidak membuat API aman.

Yang membuat API aman adalah kemampuan menjawab pertanyaan ini:

Apakah consumer lama masih bisa berjalan ketika provider baru dirilis?

Itu definisi praktis backward compatibility.

Untuk CPQ/OMS, compatibility harus dilihat dari banyak permukaan:

SurfaceContohRisiko
HTTP APIPOST /quotes, GET /orders/{id}UI, partner, integration client gagal
JSON Schemafield required, enum, formatpayload valid kemarin menjadi invalid hari ini
Error Contracterror code berubahclient tidak bisa recovery
Event ContractOrderSubmitted, QuoteAcceptedconsumer Kafka gagal deserialize
Workflow VariablesCamunda/Zeebe variablesrunning process instance gagal saat worker baru membaca variable lama
Database Shapemigration kolomdeployment gagal atau data lama tidak valid
Cache KeyRedis key schema berubahstale read, miss besar, atau data salah
Idempotency Recordrequest hash berubahduplicate command tidak dikenali
Audit Contractaudit field berubahcompliance evidence sulit dibaca

Jadi aturan pertama:

Versioning API tidak boleh dipikirkan terpisah dari schema, event, workflow, database, cache, dan audit.


2. Jangan Jadikan /v2 Sebagai Tempat Sampah Perubahan

Banyak tim memakai pendekatan ini:

Ada breaking change? Buat /v2.
Ada naming kurang enak? Buat /v2.
Ada field baru? Buat /v2.
Ada response berubah? Buat /v2.

Hasilnya:

/v1 masih dipakai UI lama
/v2 dipakai mobile app
/v3 dipakai partner
/v4 dipakai internal tool
/v5 setengah jadi

Lalu setiap bug fix harus dipikirkan lima kali.

Di enterprise CPQ/OMS, ini berbahaya karena order lifecycle bisa panjang. Satu order bisa hidup beberapa hari, minggu, atau bulan. Quote bisa direvisi berkali-kali. Approval bisa menunggu manusia. Fulfillment bisa pending karena eksternal system.

Kalau API lifecycle terlalu agresif, sistem menjadi sulit dioperasikan.

Prinsip kita:

Major API version is a compatibility boundary, not a release number.

Artinya:

  • jangan naik major hanya karena implementasi berubah,
  • jangan naik major hanya karena field baru ditambahkan,
  • jangan naik major hanya karena internal model berubah,
  • naik major hanya jika consumer lama memang tidak bisa dipertahankan dengan wajar.

3. Definisi Compatibility untuk Seri Ini

Kita akan memakai empat definisi.

3.1 Backward Compatible

Provider baru tetap bisa melayani consumer lama.

Contoh aman:

{
  "quoteId": "quo_1001",
  "status": "DRAFT",
  "validUntil": "2026-08-01T00:00:00Z",
  "approvalRequired": false
}

Field approvalRequired baru ditambahkan di response. Consumer lama yang mengabaikan field tidak dikenal tetap berjalan.

3.2 Forward Tolerant

Consumer baru masih bisa menerima data dari provider lama, selama field baru diperlakukan optional.

Contoh:

boolean approvalRequired = response.approvalRequired() != null
    ? response.approvalRequired()
    : false;

Consumer tidak boleh menganggap provider selalu mengirim field baru.

3.3 Wire Compatible

Payload lama masih bisa diparse oleh schema/DTO baru.

Contoh aman:

Field optional ditambahkan.
Enum lama tetap ada.
Existing field tidak berubah tipe.

3.4 Semantic Compatible

Makna bisnis tidak berubah untuk field/endpoint yang sama.

Ini yang paling sering dilanggar.

Contoh breaking secara semantic:

GET /quotes/{id}/price

Sebelumnya mengembalikan price termasuk discount. Sekarang mengembalikan price sebelum discount, tetapi field tetap bernama totalRecurring.

Secara schema mungkin compatible. Secara bisnis rusak.

Untuk CPQ/OMS, semantic compatibility lebih penting daripada sekadar JSON compatibility.


4. Compatibility Matrix

Gunakan matrix ini untuk menilai setiap perubahan API.

4.1 Response Body

PerubahanAman?Catatan
Menambah optional fieldYaConsumer harus ignore unknown fields
Menghapus fieldTidakBreaking
Mengubah field typeTidakBreaking
Mengubah field formatBiasanya tidakContoh date menjadi date-time
Mengubah field meaningTidakSemantic breaking
Menambah enum valueTergantungAman hanya jika consumer didesain unknown-safe
Mengubah enum value stringTidakBreaking
Mengubah nullability dari nullable ke non-nullTergantungBisa aman di response, tapi hati-hati consumer
Mengubah nullability dari non-null ke nullableTidakConsumer bisa NPE
Mengubah array orderingTergantungBreaking jika ordering pernah dijanjikan
Mengubah default sortingTidak amanBisa merusak pagination

4.2 Request Body

PerubahanAman?Catatan
Menambah optional request fieldYaProvider lama akan ignore hanya jika schema longgar; hati-hati
Menambah required request fieldTidakConsumer lama tidak mengirim
Menghapus required fieldTergantungBisa aman tapi mengubah semantic
Mengubah field typeTidakBreaking
Memperketat validationBiasanya tidakRequest lama bisa ditolak
Melonggarkan validationBiasanya amanTetapi cek invariant bisnis
Menambah enum value di requestAman bagi providerConsumer lama belum tentu tahu
Menghapus enum valueTidakBreaking

4.3 HTTP Status dan Error

PerubahanAman?Catatan
Menambah error code baruTergantungConsumer harus punya fallback unknown error
Mengganti error code lamaTidakBreaking recovery logic
Mengubah 400 menjadi 422TergantungBisa breaking jika client branch by status
Mengubah 409 conflict menjadi 200 successTidakSemantic breaking
Menambah header metadataYaJika optional
Menghapus correlation headerTidakObservability breaking

4.4 Pagination, Filtering, Sorting

PerubahanAman?Catatan
Menambah filter baruYaOptional
Menghapus filterTidakBreaking
Mengubah default page sizeTergantungBisa berdampak performance/UI
Mengubah cursor formatTidakCursor lama bisa gagal
Mengubah sort defaultTidak amanBisa duplicate/missing item di client
Menambah sortable fieldYaOptional

4.5 Event Contract

PerubahanAman?Catatan
Menambah optional fieldYaConsumer ignore unknown
Menghapus fieldTidakBreaking
Mengubah event nameTidakBreaking
Mengubah partition keySangat tidak amanOrdering bisa rusak
Mengubah event meaningTidakConsumer salah mengambil aksi
Menambah event type baruYa, jika consumer unknown-safeConsumer harus skip unknown event

5. Versioning Surfaces dalam CPQ/OMS

Jangan hanya versioning URL.

Kita perlu versioning policy untuk semua contract berikut:

Untuk seri ini, kita pakai strategi berikut.

SurfaceStrategy
Public REST APIMajor version di base path: /api/v1
Internal REST APISama, tapi lifecycle lebih cepat dan consumer terbatas
JSON Schema$id mengandung domain, object, dan major version
EventEvent name stabil, payload punya schemaVersion
Workflow BPMNVersion by deployment; process definition version dikelola Camunda/Zeebe
Worker ContractJob type stabil; variables backward-compatible
DatabaseMigration incremental, backward-compatible during rolling deploy
Redis KeyPrefix version saat shape berubah
AuditrecordSchemaVersion untuk long-term readability

6. URL Versioning: Kenapa Kita Pakai /api/v1

Ada beberapa strategi:

/api/v1/quotes
/api/quotes dengan Accept: application/vnd.company.quote.v1+json
/api/quotes?version=1
/api/quotes tanpa versi eksplisit

Untuk sistem enterprise internal + partner-heavy, /api/v1 lebih eksplisit dan mudah dioperasikan.

Kelebihan:

  • mudah dibaca,
  • mudah di-route di gateway,
  • mudah diamati di log,
  • mudah didokumentasikan,
  • mudah dipisahkan oleh contract test,
  • mudah diberi deprecation policy.

Kekurangan:

  • bisa mendorong tim membuat /v2 terlalu cepat,
  • endpoint path menjadi membawa versi,
  • kadang versi resource dan versi operation tidak selalu sejalan.

Keputusan seri:

Use /api/v1 for major compatibility boundaries.
Do not create /api/v2 for additive changes.

Contoh:

openapi: 3.1.1
info:
  title: Enterprise CPQ Quote API
  version: 1.4.0
servers:
  - url: https://api.example.com/api/v1
paths:
  /quotes:
    post:
      operationId: createQuote

Perhatikan:

  • path major version: /api/v1,
  • OpenAPI document version: 1.4.0,
  • schema version: bisa berbeda,
  • application release version: bisa berbeda.

Jangan campur semua menjadi satu angka.


7. Jangan Samakan API Version dengan Application Version

Misalnya:

Application release: 2026.07.15-rc.3
API document version: 1.8.0
API base path: /api/v1
Quote schema major: 1
Order event schema major: 1
Workflow version: quote-approval:17
Database migration: V142__add_quote_approval_reason.sql

Semua angka itu menjawab pertanyaan berbeda.

VersionMenjawab Pertanyaan
Application releaseBuild/deployment mana yang berjalan?
API document versionKontrak OpenAPI berubah apa?
API major pathApakah consumer lama masih compatible?
Schema majorApakah payload shape masih compatible?
Event schema versionConsumer event harus membaca versi mana?
Workflow versionInstance baru memakai BPMN deployment mana?
DB migrationStruktur database sudah sampai tahap mana?

Kalau semua dipaksa satu angka, release governance akan kacau.


8. Semantic Versioning untuk API Document

Kita bisa memakai ide SemVer untuk info.version OpenAPI, tetapi dengan interpretasi contract:

MAJOR.MINOR.PATCH
BagianArti dalam API Contract
MAJORBreaking contract change
MINORBackward-compatible addition
PATCHClarification, documentation fix, non-contract correction

Contoh:

1.2.0 -> 1.3.0

Menambah optional field quote.expirationReason di response.

1.3.0 -> 2.0.0

Mengubah quote.status dari string enum menjadi object.

Tapi jangan otomatis deploy /api/v2 hanya karena info.version major naik. Path major dan document major biasanya sejalan, tetapi proses migrasinya perlu desain.


9. JSON Schema Versioning Policy

Gunakan $id yang stabil dan eksplisit.

Contoh:

{
  "$id": "https://schemas.example.com/cpq/quote/v1/quote-response.schema.json",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "QuoteResponse",
  "type": "object",
  "required": ["quoteId", "status", "items", "currency"],
  "properties": {
    "quoteId": { "type": "string", "pattern": "^quo_[A-Za-z0-9]+$" },
    "status": {
      "type": "string",
      "enum": ["DRAFT", "PRICED", "SUBMITTED", "APPROVED", "ACCEPTED", "EXPIRED", "CANCELLED"]
    },
    "currency": { "type": "string", "minLength": 3, "maxLength": 3 },
    "items": {
      "type": "array",
      "items": { "$ref": "quote-item.schema.json" }
    }
  },
  "additionalProperties": false
}

Policy:

v1 in $id means major schema version.
Minor/patch changes live in repository tag or OpenAPI info.version.

Kenapa tidak menaruh minor di $id?

Karena kalau setiap optional field baru membuat URL schema berubah, consumer harus terlalu sering update. Untuk runtime validation, biasanya kita butuh major compatibility boundary, bukan setiap patch.


10. additionalProperties: Strict atau Loose?

Di Part 016 kita sudah menyentuh ini. Untuk versioning, ini sangat penting.

Ada dua pilihan:

Strict Contract

{
  "type": "object",
  "additionalProperties": false
}

Kelebihan:

  • payload liar ditolak,
  • typo cepat terdeteksi,
  • domain contract rapi,
  • audit lebih bersih.

Kekurangan:

  • provider lama bisa menolak request consumer baru yang mengirim optional field baru,
  • extension sulit.

Loose Contract

{
  "type": "object",
  "additionalProperties": true
}

Kelebihan:

  • forward tolerant,
  • cocok untuk extension.

Kekurangan:

  • typo lolos,
  • field tidak terdokumentasi menyebar,
  • domain model bisa bocor.

Keputusan seri:

Payload TypePolicy
Public command requestStrict
Public responseStrict di schema, consumer harus ignore unknown jika library memungkinkan
Event payloadPrefer strict per major version
Metadata bagExplicit extension object
Partner extensionGunakan extensions object, bukan root loose field

Contoh extension terkendali:

{
  "quoteId": "quo_1001",
  "status": "DRAFT",
  "extensions": {
    "partnerX": {
      "campaignCode": "RAMADAN-2026"
    }
  }
}

Root object tetap terkendali.


11. Enum Evolution: Sumber Breaking Change Diam-Diam

Enum terlihat sederhana, tetapi sering merusak consumer.

Contoh:

{
  "status": "PENDING_EXTERNAL_APPROVAL"
}

Jika consumer lama hanya mengenal:

enum QuoteStatus {
    DRAFT,
    PRICED,
    SUBMITTED,
    APPROVED,
    REJECTED,
    ACCEPTED
}

Deserialize bisa gagal.

Ada tiga strategi.

11.1 Closed Enum

Consumer harus mengenal semua value.

Cocok untuk command request:

Client mengirim actionType.
Provider berhak menolak value tidak dikenal.

11.2 Open Enum

Consumer harus toleran terhadap value baru.

Cocok untuk response status:

Server dapat memperkenalkan status baru.
Client lama harus fallback ke UNKNOWN.

11.3 Enum + Category

Untuk status yang kemungkinan berkembang, tambahkan category stabil.

{
  "status": "PENDING_EXTERNAL_APPROVAL",
  "statusCategory": "IN_PROGRESS"
}

UI lama bisa memakai statusCategory untuk behavior umum.

Contoh Java:

public enum StatusCategory {
    DRAFT,
    IN_PROGRESS,
    SUCCESS,
    FAILURE,
    TERMINAL,
    UNKNOWN
}

Untuk CPQ/OMS, ini sangat berguna di order dan fulfillment dashboard.


12. Error Code Versioning

Error code adalah contract.

Jangan asal ubah.

Contoh error:

{
  "type": "https://errors.example.com/cpq/quote/invalid-transition",
  "title": "Invalid quote state transition",
  "status": 409,
  "code": "QUOTE_INVALID_STATE_TRANSITION",
  "detail": "Quote quo_1001 cannot move from ACCEPTED to DRAFT.",
  "correlationId": "corr_abc",
  "violations": []
}

Policy:

  • code stabil,
  • type stabil,
  • title boleh diperjelas tapi jangan ubah makna,
  • detail tidak boleh diparse client,
  • violations.field harus stabil jika digunakan UI,
  • error baru boleh ditambahkan,
  • error lama jangan diganti diam-diam.

Kalau perlu mengganti error:

1. Tambahkan error baru.
2. Dokumentasikan mapping lama -> baru.
3. Emit warning/deprecation header jika memungkinkan.
4. Hapus hanya di major version berikutnya.

13. Idempotency Contract Compatibility

Idempotency sering ikut rusak saat request schema berubah.

Misalnya di /quotes/{id}/submit kita menyimpan:

idempotencyKey + requestHash -> response

Jika field optional baru ditambahkan:

{
  "submittedBy": "usr_1",
  "comment": "Please approve",
  "clientContext": {
    "source": "partner-portal"
  }
}

Request hash bisa berbeda walau command semantic sama.

Policy:

Hash only semantic command fields.
Ignore non-semantic metadata for idempotency identity.
Store full request payload for audit separately.

Contoh:

FieldMasuk Hash?
quoteIdYa
requestedTransitionYa
submittedByYa
commentTergantung policy
clientContext.sourceTidak
correlationIdTidak
requestTimestampTidak

Kalau idempotency hash berubah karena field metadata, retry bisa dianggap command baru. Itu berbahaya.


14. Versioning untuk Async Command

Di sistem CPQ/OMS, banyak command tidak selesai langsung.

Contoh:

POST /api/v1/orders/{orderId}/submit

Response:

{
  "commandId": "cmd_123",
  "orderId": "ord_1001",
  "status": "ACCEPTED",
  "statusUrl": "/api/v1/commands/cmd_123"
}

Masalah compatibility:

  • command response version,
  • command status projection version,
  • final order projection version,
  • event emitted setelah command.

Policy:

Async command contract must be stable even if internal workflow changes.

Jangan pernah membocorkan internal Camunda element ID atau Kafka offset sebagai public contract.

Buruk:

{
  "zeebeProcessInstanceKey": 2251799813685250,
  "topic": "order-command-v2",
  "partition": 4,
  "offset": 91231
}

Lebih baik:

{
  "commandId": "cmd_123",
  "resourceType": "ORDER",
  "resourceId": "ord_1001",
  "status": "ACCEPTED",
  "statusUrl": "/api/v1/commands/cmd_123"
}

Internal runtime boleh berubah tanpa memaksa consumer berubah.


15. Event Contract Versioning

Kafka event bukan API kelas dua. Ia juga contract.

Event envelope:

{
  "eventId": "evt_1001",
  "eventType": "OrderSubmitted",
  "schemaVersion": "1.0",
  "occurredAt": "2026-07-02T10:15:30Z",
  "producer": "order-service",
  "correlationId": "corr_abc",
  "aggregateType": "ORDER",
  "aggregateId": "ord_1001",
  "payload": {
    "orderId": "ord_1001",
    "status": "SUBMITTED"
  }
}

Event compatibility rules:

ChangeRule
Add optional payload fieldMinor-compatible
Remove payload fieldBreaking
Rename event typeBreaking
Change aggregate ID formatBreaking
Change partition keyTreat as major operational change
Change meaning of eventBreaking
Add event typeCompatible if consumer ignores unknown

Satu kesalahan umum:

OrderSubmitted dulu berarti order valid dan siap fulfillment.
Sekarang OrderSubmitted berarti hanya diterima API.

Itu breaking change walaupun payload sama.

Gunakan event baru:

OrderSubmissionAccepted
OrderValidated
OrderFulfillmentStarted

Jangan mengubah makna event lama.


16. Event Replay dan Historical Payload

Dalam event-driven system, consumer bisa membaca event lama.

Artinya worker/consumer baru harus bisa membaca payload lama.

Strategi:

Deserialize old schema -> map to internal canonical model -> process using current logic if safe.

Jangan proses langsung dari DTO event ke domain behavior tanpa translation layer.

Translation layer membuat consumer lebih tahan terhadap evolusi.


17. Workflow Variable Compatibility

Camunda 8/Zeebe workflow bisa long-running.

Masalah:

  • process instance lama menyimpan variables lama,
  • worker baru membaca variable lama,
  • BPMN baru mungkin punya task berbeda,
  • incident bisa dibuat dari version lama.

Policy:

Workflow variables are persisted contracts.
Treat them like event payloads.

Contoh buruk:

{
  "quote": {
    "id": "quo_1",
    "items": [...],
    "price": {...}
  }
}

Workflow menyimpan seluruh aggregate. Jika domain berubah, variable lama besar dan rapuh.

Lebih baik:

{
  "quoteId": "quo_1",
  "quoteVersion": 7,
  "approvalPolicySnapshotId": "aps_22",
  "approvalContext": {
    "requiresPriceOverrideApproval": true,
    "requiresMarginApproval": false
  }
}

Worker dapat reload aggregate dari service/database dengan version guard.


18. Worker Contract Versioning

Zeebe job type adalah contract antara BPMN dan worker.

Contoh:

jobType = calculate-price
jobType = submit-approval-request
jobType = reserve-resource

Jangan ubah behavior job type secara breaking.

Jika task berubah besar, buat job type baru:

calculate-price-v2

Tapi jangan terlalu cepat menambahkan suffix version. Gunakan jika input/output semantic berubah.

Worker harus:

  • menerima variable lama,
  • memberi default untuk field baru,
  • menulis output variable yang backward-compatible,
  • membedakan business error dan technical retry,
  • idempotent terhadap job retry.

19. Database Migration Compatibility

API compatibility akan gagal jika database migration tidak mendukung rolling deploy.

Pattern aman:

Expand -> Migrate -> Contract

19.1 Expand

Tambahkan struktur baru tanpa menghapus struktur lama.

ALTER TABLE quote
ADD COLUMN approval_required BOOLEAN;

Pastikan aplikasi lama tetap berjalan.

19.2 Migrate

Isi data baru.

UPDATE quote
SET approval_required = false
WHERE approval_required IS NULL;

19.3 Contract

Setelah semua app versi lama mati, baru perketat.

ALTER TABLE quote
ALTER COLUMN approval_required SET NOT NULL;

Dalam deployment enterprise, contract step bisa terjadi release berikutnya, bukan di release yang sama.


20. Redis Cache Versioning

Cache key juga contract internal.

Contoh key:

catalog:v1:offering:po_1001
pricing:v1:price-list:pl_2026_idr
quote-summary:v1:quo_1001

Jika shape value berubah secara incompatible:

catalog:v2:offering:po_1001

Jangan recycle key lama untuk value shape baru jika aplikasi lama dan baru bisa berjalan bersamaan.

Rule:

PerubahanCache Strategy
Add optional fieldBisa tetap v1
Change value shapePrefix baru
Change serialization formatPrefix baru
Change TTL onlyTidak perlu prefix baru
Change semantic meaningPrefix baru

Cache bug sering tampak seperti domain bug. Version prefix membuat investigasi lebih mudah.


21. Deprecation Policy

Deprecation tanpa tanggal dan owner hanyalah komentar.

Kita butuh policy eksplisit:

1. Announce deprecation.
2. Emit deprecation metadata.
3. Monitor usage.
4. Provide migration guide.
5. Support overlap period.
6. Remove only after consumer migration or approved exception.

Header contoh:

Deprecation: true
Sunset: Wed, 30 Sep 2026 23:59:59 GMT
Link: <https://developer.example.com/migrations/quote-v1-discount-fields>; rel="deprecation"

Jangan gunakan deprecation sebagai ancaman. Gunakan sebagai operational contract.

Tracking table:

Deprecated ElementReplacementOwnerConsumersLast SeenSunset
quote.totalDiscountquote.priceSummary.discountTotalCPQ API TeamPartner A, UI Old2026-07-012026-09-30

22. Consumer Compatibility by Design

Provider tidak bisa menjaga compatibility sendirian. Consumer juga harus didesain tahan perubahan.

Consumer rules:

Ignore unknown response fields.
Do not parse human-readable detail.
Do not depend on object property order.
Do not assume enum is closed unless documented.
Do not assume pagination default ordering beyond contract.
Do not depend on undocumented fields.
Do not deserialize event directly into domain aggregate.

Java client DTO example:

public record QuoteSummaryResponse(
    String quoteId,
    String quoteNumber,
    String status,
    String statusCategory,
    String currency,
    MoneyResponse totalOneTime,
    MoneyResponse totalRecurring
) {
    public StatusCategory safeStatusCategory() {
        try {
            return StatusCategory.valueOf(statusCategory);
        } catch (Exception ex) {
            return StatusCategory.UNKNOWN;
        }
    }
}

Jika memakai library JSON yang strict terhadap unknown property, atur agar consumer response lebih toleran.


23. Provider Compatibility by Design

Provider rules:

Never remove documented field in same major version.
Never change documented field meaning.
Never make optional request field required.
Never change error code semantic.
Never change idempotency identity without migration.
Never change event partition key casually.
Never expose internal runtime identifiers as public API.

Provider juga harus punya compatibility test.


24. Contract Diff Pipeline

Di CI/CD, setiap perubahan OpenAPI dan JSON Schema harus dicek.

Pipeline:

Breaking change detection tidak sempurna. Tool bisa menemukan structural breaking change, tetapi semantic breaking change tetap perlu review manusia.

Contoh yang tool bisa deteksi:

  • field dihapus,
  • required field ditambah,
  • type berubah,
  • path dihapus,
  • response status dihapus.

Contoh yang tool sulit deteksi:

  • totalRecurring berubah dari net ke gross,
  • OrderSubmitted berubah makna,
  • default sort berubah,
  • timeout SLA berubah,
  • approval policy berubah.

Karena itu PR contract harus punya section:

Semantic Compatibility Notes

25. Contract Artifact Repository

Struktur repository contract:

contracts/
  openapi/
    cpq/
      quote-api.v1.yaml
      pricing-api.v1.yaml
      configuration-api.v1.yaml
    oms/
      order-api.v1.yaml
      fulfillment-api.v1.yaml
  schemas/
    common/v1/
      money.schema.json
      problem.schema.json
      page.schema.json
    cpq/v1/
      quote-response.schema.json
      quote-command.schema.json
    oms/v1/
      order-response.schema.json
      order-event.schema.json
  events/
    oms/v1/
      order-submitted.event.schema.json
      order-completed.event.schema.json
  compatibility/
    allowed-breaking-changes.yml
    deprecations.yml

Setiap service build mengambil contract dari artifact version tertentu, bukan copy manual tak terkendali.


26. Java Implementation Boundary

Jangan membuat JAX-RS resource langsung memakai domain aggregate.

Buruk:

@GET
@Path("/{quoteId}")
public Quote getQuote(@PathParam("quoteId") String quoteId) {
    return quoteService.getQuote(quoteId);
}

Masalah:

  • domain object bocor,
  • perubahan domain menjadi perubahan API,
  • field internal bisa terekspos,
  • compatibility sulit dikontrol.

Lebih baik:

@GET
@Path("/{quoteId}")
public QuoteResponse getQuote(@PathParam("quoteId") String quoteId) {
    QuoteView view = quoteQueryService.getQuoteView(QuoteId.of(quoteId));
    return quoteResponseMapper.toV1Response(view);
}

Boundary:

JAX-RS Resource -> Application Service / Query Service -> Domain Model / Query Model -> API Mapper -> DTO

Mapper menjadi compatibility wall.


27. Package Strategy untuk Versioned API

Contoh package:

com.example.cpq.quote.api.v1.resource
com.example.cpq.quote.api.v1.dto
com.example.cpq.quote.api.v1.mapper
com.example.cpq.quote.application
com.example.cpq.quote.domain
com.example.cpq.quote.persistence

Jika /v2 benar-benar perlu dibuat:

com.example.cpq.quote.api.v2.resource
com.example.cpq.quote.api.v2.dto
com.example.cpq.quote.api.v2.mapper

Domain tidak otomatis di-copy ke v2. Domain boleh tetap satu jika capability sama.

Ini menghindari duplikasi business logic.


28. Backward-Compatible Additive Change: Contoh Lengkap

Kebutuhan baru:

Quote response perlu menampilkan approvalSummary.

28.1 Schema Change

{
  "approvalSummary": {
    "type": "object",
    "properties": {
      "approvalRequired": { "type": "boolean" },
      "pendingApprovalCount": { "type": "integer", "minimum": 0 },
      "highestApprovalLevel": { "type": ["string", "null"] }
    },
    "required": ["approvalRequired", "pendingApprovalCount"],
    "additionalProperties": false
  }
}

Field ditambahkan sebagai optional di root.

28.2 OpenAPI Version

1.7.0 -> 1.8.0

Minor bump.

28.3 Database

Jika perlu kolom baru:

ALTER TABLE quote
ADD COLUMN approval_summary_json JSONB;

Atau lebih baik dari query model existing jika bisa dihitung.

28.4 Java Mapper

public QuoteResponse toV1Response(QuoteView view) {
    return new QuoteResponse(
        view.quoteId().value(),
        view.status().name(),
        mapMoney(view.totalOneTime()),
        mapMoney(view.totalRecurring()),
        mapApprovalSummary(view.approvalSummary())
    );
}

28.5 Compatibility Review

Consumer lama: ignore field baru.
Consumer baru: field optional; handle null.
Event: tidak berubah.
Workflow: tidak berubah.
DB: expand-only.
Cache: value shape bertambah optional, prefix tetap v1 jika deserializer toleran.

29. Breaking Change: Contoh dan Cara Migrasi

Kebutuhan:

quote.status string tidak cukup. Kita butuh status object berisi code, category, reason, dan actor.

Breaking jika langsung mengubah:

{
  "status": {
    "code": "SUBMITTED",
    "category": "IN_PROGRESS"
  }
}

Karena consumer lama mengharapkan string.

Strategi aman di v1:

{
  "status": "SUBMITTED",
  "statusDetail": {
    "code": "SUBMITTED",
    "category": "IN_PROGRESS",
    "reason": "WAITING_APPROVAL",
    "changedBy": "usr_123"
  }
}

Kemudian jika suatu hari /v2 dibuat:

{
  "status": {
    "code": "SUBMITTED",
    "category": "IN_PROGRESS",
    "reason": "WAITING_APPROVAL",
    "changedBy": "usr_123"
  }
}

Migration path:

1. Add statusDetail to v1.
2. Ask consumers to migrate.
3. Monitor usage of old status-only behavior.
4. Introduce v2 only when justified.
5. Keep v1 until sunset policy complete.

30. Version Negotiation: Kapan Dibutuhkan?

Untuk sebagian besar API kita, base path cukup.

Tetapi ada kasus version negotiation berguna:

  • export file,
  • partner-specific payload,
  • webhook callback,
  • event replay endpoint,
  • report projection.

Contoh header:

Accept: application/vnd.example.quote-summary.v1+json

Namun jangan campur terlalu banyak mekanisme.

Keputusan seri:

Primary: path major version.
Secondary: media type only for specialized representations.
Never: query parameter version for core API.

31. Compatibility dengan Partner Lambat

Enterprise partner sering tidak bisa upgrade cepat.

Mereka mungkin punya:

  • release window per kuartal,
  • testing cycle panjang,
  • manual certification,
  • middleware lama,
  • fixed schema mapping,
  • batch integration.

Jadi compatibility policy harus realistis.

Praktik:

Public partner API lebih stabil daripada internal API.
Partner-specific adapter boleh hidup lebih lama daripada canonical internal API.
Anti-corruption layer menahan legacy contract.

Jangan memaksa domain internal menjadi bentuk partner lama.

Gunakan adapter:


32. Compatibility dengan UI Internal

UI sering dianggap mudah diubah karena satu perusahaan. Kenyataannya tidak selalu.

UI bisa punya:

  • cached bundle,
  • mobile app release delay,
  • browser tab lama,
  • feature flag,
  • role-specific workflow,
  • customer service training dependency.

Karena itu API jangan sengaja breaking hanya karena consumer internal.

Untuk UI, gunakan pattern:

Stable command API + projection tailored for screen.

Jika screen berubah, bisa buat projection baru tanpa mengubah command contract.

Contoh:

GET /api/v1/quotes/{id}
GET /api/v1/quotes/{id}/sales-workbench-view
GET /api/v1/quotes/{id}/approval-workbench-view

Projection bukan domain aggregate.


33. Backward Compatibility Test Suite

Test minimal:

TestTujuan
OpenAPI diff testDeteksi breaking structural change
JSON Schema validation testPayload contoh lama masih valid
Golden response testResponse lama masih bisa diproduksi
Consumer contract testConsumer critical masih jalan
Event replay testEvent lama bisa dibaca consumer baru
Workflow variable migration testWorker baru bisa membaca variable lama
DB rolling migration testApp lama dan baru compatible selama deploy
Cache compatibility testKey lama tidak salah dibaca sebagai shape baru

Golden payload contoh:

test-fixtures/
  api/v1/quote-response/
    quote-draft-1.0.json
    quote-priced-1.2.json
    quote-approved-1.5.json
  events/v1/order/
    order-submitted-1.0.json
    order-completed-1.3.json
  workflow/quote-approval/
    variables-instance-v12.json

Golden payload bukan snapshot sembarang. Ia adalah compatibility asset.


34. Release Governance Checklist

Sebelum merge contract change, jawab:

1. Apakah ada field/path/error/event yang dihapus?
2. Apakah ada request field baru yang required?
3. Apakah enum ditambah? Consumer unknown-safe?
4. Apakah semantic field berubah?
5. Apakah default sorting/filtering berubah?
6. Apakah idempotency hash berubah?
7. Apakah event partition key berubah?
8. Apakah workflow variable berubah?
9. Apakah running process instance lama tetap bisa lanjut?
10. Apakah migration database expand-first?
11. Apakah cache key perlu prefix baru?
12. Apakah audit reader tetap bisa membaca record lama?
13. Apakah deprecation perlu diumumkan?
14. Apakah partner consumer terdampak?
15. Apakah migration guide dibuat?

Kalau jawaban tidak jelas, jangan merge.


35. Failure Modes

35.1 Field Required Baru Merusak Consumer Lama

Gejala:

Partner mendapat 400/422 setelah release.

Penyebab:

Provider menambahkan required request field di v1.

Pencegahan:

Tambahkan optional field, default server-side, atau buat command baru.

35.2 Enum Baru Merusak UI

Gejala:

UI blank karena status tidak dikenal.

Pencegahan:

Gunakan statusCategory dan UNKNOWN fallback.

35.3 Event Consumer Gagal Saat Replay

Gejala:

Consumer baru crash membaca event lama.

Pencegahan:

Event DTO versioned + canonical mapper + golden replay tests.

35.4 Workflow Incident Setelah Deployment

Gejala:

Running process instance lama gagal di worker baru.

Pencegahan:

Variable backward compatibility, worker defaulting, BPMN migration plan.

35.5 Cache Shape Mismatch

Gejala:

ClassCastException, missing field, stale price, atau quote summary salah.

Pencegahan:

Versioned cache prefix for incompatible value shape.

36. Architecture Decision Record

Gunakan ADR untuk versioning policy.

# ADR-017: API and Schema Versioning Policy

## Status
Accepted

## Context
CPQ/OMS API will be consumed by internal UI, partner systems, workflow workers, event consumers, and operations tools. Orders and quotes can be long-running. Breaking changes can disrupt commercial and fulfillment operations.

## Decision
Use `/api/v1` as major public API compatibility boundary. Use OpenAPI `info.version` with semantic versioning semantics. Use JSON Schema `$id` with major schema version. Treat event payloads, workflow variables, cache values, and audit records as versioned contracts.

## Consequences
Additive changes do not create `/v2`. Breaking changes require migration plan, deprecation policy, usage monitoring, and architecture approval. JAX-RS resources must map domain/query models into versioned DTOs through explicit mappers.

37. Implementation Milestone for Our Platform

Untuk build-from-scratch nanti, kita akan implementasikan minimal:

1. /api/v1 base path.
2. OpenAPI info.version.
3. Schema $id with major version.
4. Contract diff CI placeholder.
5. API DTO mappers separated from domain.
6. Event envelope with schemaVersion.
7. Workflow variable version marker.
8. Cache key prefix version.
9. Deprecation registry file.
10. Golden payload compatibility tests.

Ini bukan overhead. Ini pagar keselamatan.


38. Ringkasan

API versioning bukan tentang angka.

API versioning adalah disiplin menjaga janji.

Untuk enterprise CPQ/OMS:

Contract lives longer than implementation.
Events live longer than deployment.
Workflow instances live longer than code version.
Audit records live longer than product team memory.

Maka desain kita:

  • /api/v1 untuk major compatibility boundary,
  • OpenAPI info.version untuk evolusi dokumen,
  • JSON Schema $id untuk major schema version,
  • event envelope dengan schemaVersion,
  • workflow variable sebagai persisted contract,
  • database migration expand-migrate-contract,
  • Redis key prefix untuk incompatible shape,
  • DTO mapper sebagai compatibility wall,
  • contract diff dan golden payload test sebagai quality gate.

Part berikutnya kita akan memakai semua prinsip ini untuk melakukan mapping API yang lebih realistis terhadap vocabulary enterprise commerce, khususnya yang diinspirasi oleh TM Forum Open APIs.


39. Referensi Resmi yang Relevan

Lesson Recap

You just completed lesson 17 in build core. 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.