Series MapLesson 14 / 60
Build CoreOrdered learning track

Learn Enterprise Cpq Oms Glassfish Camunda8 Part 014 Resource Modelling For Cpq Oms Apis

12 min read2251 words
PrevNext
Lesson 1460 lesson track1233 Build Core

title: Build From Scratch: Enterprise Java Microservices CPQ & Order Management Platform - Part 014 description: Mendesain resource model API untuk catalog, configuration, pricing, quote, approval, order, fulfillment, asset, dan operational APIs agar kontrak HTTP merepresentasikan business capability, bukan sekadar CRUD table atau remote method Java. series: learn-enterprise-cpq-oms-glassfish-camunda8 seriesTitle: Build From Scratch: Enterprise Java Microservices CPQ & Order Management Platform order: 14 partTitle: Resource Modelling for CPQ OMS APIs tags:

  • java
  • microservices
  • cpq
  • oms
  • openapi
  • api-design
  • resource-modelling
  • jax-rs
  • jersey
  • contract-first
  • enterprise-architecture date: 2026-07-02

Part 014 — Resource Modelling for CPQ OMS APIs

Di Part 013 kita menyusun strategi API First dan Schema First.

Sekarang kita mulai menjawab pertanyaan yang lebih konkret:

Endpoint apa yang harus ada?
Resource apa yang harus diekspos?
Command apa yang harus menjadi sub-resource?
Kapan pakai POST, GET, PATCH, DELETE?
Kapan lifecycle transition tidak boleh dimodelkan sebagai update status?

Part ini bukan daftar endpoint final yang kaku.

Part ini adalah resource modelling guide untuk CPQ/OMS enterprise.

Tujuannya: saat nanti kita menulis OpenAPI, JAX-RS resource, MyBatis query, Camunda worker, dan Kafka event, semuanya punya boundary yang masuk akal.


1. Target Part Ini

Part ini membahas:

  1. prinsip resource modelling;
  2. kenapa CPQ/OMS API tidak boleh sekadar CRUD;
  3. resource taxonomy untuk catalog, configuration, pricing, quote, approval, order, fulfillment, asset, dan operations;
  4. command resource pattern;
  5. resource lifecycle;
  6. sync vs async command;
  7. idempotency;
  8. path naming;
  9. filtering, sorting, pagination;
  10. expansion dan projection;
  11. ETag/version handling;
  12. error resource;
  13. operational resource;
  14. API boundary terhadap Camunda, Kafka, Redis, dan database;
  15. OpenAPI shape awal;
  16. JAX-RS implementation direction;
  17. anti-patterns.

2. Resource Modelling: Definisi Praktis

Resource modelling adalah proses memilih apa yang layak menjadi konsep publik di API.

Resource bukan berarti tabel.

Resource bukan berarti class Java.

Resource adalah sesuatu yang:

has identity,
has lifecycle,
can be observed,
can be acted upon,
and has meaning to API consumers.

Contoh resource yang bagus:

ProductOffering
ProductConfiguration
Quote
QuoteItem
PricingRun
ApprovalRequest
Order
OrderItem
FulfillmentPlan
FulfillmentTask
Asset
Subscription
CancellationRequest

Contoh resource yang buruk untuk public API:

QuoteEntityRow
OrderJoinResult
WorkflowVariableBlob
OutboxRecord
RedisCacheEntry
MyBatisResultMap
ZeebeJobKey

Yang buruk di atas mungkin penting secara internal, tetapi tidak layak menjadi resource publik.


3. CRUD Tidak Cukup Untuk CPQ/OMS

CRUD cocok untuk resource sederhana.

CPQ/OMS bukan resource sederhana.

Contoh quote:

create draft
add item
configure item
validate
price
submit
approve
accept
convert to order
expire
cancel
revise

Jika semua dipaksa ke CRUD:

PATCH /quotes/{quoteId}
{
  "status": "APPROVED"
}

Maka API kehilangan meaning.

Apa yang hilang?

  • siapa approver;
  • decision reason;
  • approval policy;
  • state transition guard;
  • audit evidence;
  • idempotency;
  • event emission;
  • workflow correlation;
  • rejection reason;
  • approval expiry;
  • price snapshot lock.

Business transition bukan field update.

Business transition harus dimodelkan sebagai command/resource yang eksplisit.


4. Resource Taxonomy

Kita kelompokkan resource berdasarkan bounded context.

Resource taxonomy ini akan berkembang, tetapi cukup sebagai starting point.


5. API Surface Berdasarkan Capability

5.1 Catalog API

Tujuan: consumer bisa menemukan apa yang bisa dijual.

Resource:

/product-offerings
/product-offerings/{productOfferingId}
/product-specifications
/product-specifications/{productSpecificationId}
/catalog-versions
/catalog-versions/{catalogVersionId}

Capability:

  • browse offering;
  • search offering;
  • resolve offering detail;
  • inspect characteristics;
  • inspect price references;
  • inspect compatibility metadata;
  • inspect lifecycle state.

Tidak semua catalog management perlu public.

Catalog publishing/admin API bisa internal.

5.2 Configuration API

Tujuan: consumer bisa membangun konfigurasi produk yang valid.

Resource:

/configuration-sessions
/configuration-sessions/{configurationSessionId}
/configuration-sessions/{configurationSessionId}/items
/configuration-sessions/{configurationSessionId}/validation-runs

Atau jika configuration selalu melekat ke quote:

/quotes/{quoteId}/items/{quoteItemId}/configuration
/quotes/{quoteId}/items/{quoteItemId}/configuration-validation-runs

Seri ini akan memakai kombinasi:

  • quote item punya configuration snapshot;
  • standalone configuration session bisa dipakai untuk pre-quote simulation;
  • final quote tetap menyimpan immutable snapshot.

5.3 Pricing API

Tujuan: consumer bisa menjalankan price simulation atau price quote item.

Resource:

/pricing-runs
/pricing-runs/{pricingRunId}
/quotes/{quoteId}/pricing-runs
/quotes/{quoteId}/pricing-runs/{pricingRunId}

Pricing run adalah resource karena:

  • punya input snapshot;
  • punya output breakdown;
  • bisa audit;
  • bisa explain;
  • bisa gagal;
  • bisa dibandingkan antar revision.

5.4 Quote API

Tujuan: consumer bisa mengelola commercial promise.

Resource:

/quotes
/quotes/{quoteId}
/quotes/{quoteId}/items
/quotes/{quoteId}/items/{quoteItemId}
/quotes/{quoteId}/revisions
/quotes/{quoteId}/pricing-runs
/quotes/{quoteId}/submission-requests
/quotes/{quoteId}/acceptance-requests
/quotes/{quoteId}/cancellation-requests
/quotes/{quoteId}/conversion-requests

5.5 Approval API

Tujuan: approver bisa melihat request dan memberikan decision.

Resource:

/approval-requests
/approval-requests/{approvalRequestId}
/approval-requests/{approvalRequestId}/decisions
/approval-requests/{approvalRequestId}/escalations

Approval bukan sekadar status quote.

Approval punya:

  • requester;
  • approver;
  • policy;
  • decision;
  • reason;
  • SLA;
  • delegation;
  • escalation;
  • evidence.

5.6 Order API

Tujuan: consumer bisa membuat order, memantau order, dan meminta perubahan order.

Resource:

/orders
/orders/{orderId}
/orders/{orderId}/items
/orders/{orderId}/items/{orderItemId}
/orders/{orderId}/cancellation-requests
/orders/{orderId}/amendment-requests
/orders/{orderId}/events
/orders/{orderId}/timeline

Order API harus sangat hati-hati.

Order adalah execution commitment, bukan shopping cart.

5.7 Fulfillment API

Tujuan: internal operations/support bisa melihat execution plan.

Resource:

/fulfillment-plans
/fulfillment-plans/{fulfillmentPlanId}
/fulfillment-plans/{fulfillmentPlanId}/tasks
/fulfillment-tasks/{fulfillmentTaskId}
/fulfillment-tasks/{fulfillmentTaskId}/retry-requests
/fulfillment-tasks/{fulfillmentTaskId}/manual-completion-requests

Fulfillment API biasanya internal/operations, bukan partner public.

5.8 Asset and Subscription API

Tujuan: customer care, CPQ, OMS, billing, dan support bisa melihat installed base.

Resource:

/assets
/assets/{assetId}
/assets/{assetId}/versions
/assets/{assetId}/relationships
/subscriptions
/subscriptions/{subscriptionId}
/subscriptions/{subscriptionId}/versions

Asset mutation sebaiknya lewat order, bukan direct edit public API.

Direct repair API harus controlled.

5.9 Operational API

Tujuan: support dan SRE bisa melihat dan memperbaiki masalah dengan aman.

Resource:

/operations/incidents
/operations/incidents/{incidentId}
/operations/repair-actions
/operations/repair-actions/{repairActionId}
/operations/reconciliation-runs
/operations/reconciliation-runs/{reconciliationRunId}

Operational API harus punya audit lebih ketat daripada API biasa.


6. Command Resource Pattern

Business command sebaiknya dimodelkan sebagai resource baru.

Contoh:

POST /quotes/{quoteId}/submission-requests

Request:

{
  "requestedBy": "usr_01JZ80Q1QQN8A69KJGF3SRBSP4",
  "comment": "Customer is ready for approval review."
}

Response:

{
  "id": "qsr_01JZ80Q6M9Y4TQR5HH6K8PN1V6",
  "quoteId": "quo_01JZ7R7Y95W6PPXN3DWV4K8FQG",
  "status": "ACCEPTED_FOR_PROCESSING",
  "resultingQuoteStatus": "APPROVAL_REQUIRED",
  "approvalRequestId": "apr_01JZ80R4JK3BGEP66WCR8QVE7Z",
  "createdAt": "2026-07-02T10:20:30+07:00"
}

Kenapa bukan POST /quotes/{quoteId}/submit?

/submit masih terlihat seperti RPC.

/submission-requests menyatakan bahwa kita membuat sebuah request yang bisa:

  • diterima;
  • ditolak;
  • diproses async;
  • punya ID;
  • punya audit;
  • punya status;
  • aman untuk retry dengan idempotency key.

Tidak semua command harus begini, tetapi untuk business transition penting, pola ini kuat.


7. POST, PUT, PATCH, DELETE: Aturan Praktis

7.1 GET

Untuk membaca resource.

GET /quotes/{quoteId}
GET /orders/{orderId}
GET /product-offerings?marketSegment=ENTERPRISE

GET harus safe.

Jangan trigger pricing calculation berat lewat GET jika hasilnya membuat record/audit.

7.2 POST

Untuk membuat resource atau command request.

POST /quotes
POST /quotes/{quoteId}/items
POST /quotes/{quoteId}/pricing-runs
POST /quotes/{quoteId}/submission-requests
POST /orders
POST /orders/{orderId}/cancellation-requests

POST mutating wajib pakai idempotency key kecuali memang tidak bisa diulang dan didokumentasikan jelas.

7.3 PUT

Untuk replace resource by known identity.

Jarang dipakai di CPQ/OMS public API.

Cocok untuk admin/reference data tertentu:

PUT /internal/catalog/product-offerings/{productOfferingId}

Tetapi untuk quote/order lifecycle, PUT jarang tepat.

7.4 PATCH

Untuk partial update field yang memang bukan transition kompleks.

Contoh masuk akal:

PATCH /quotes/{quoteId}
{
  "customerContactId": "cnt_01JZ8..."
}

Tetapi jangan:

PATCH /quotes/{quoteId}
{
  "status": "ACCEPTED"
}

Status transition harus command.

7.5 DELETE

Untuk delete resource yang benar-benar boleh dihapus.

Di enterprise CPQ/OMS, delete sering diganti cancel/archive karena audit.

Contoh:

POST /quotes/{quoteId}/cancellation-requests

lebih aman daripada:

DELETE /quotes/{quoteId}

8. Quote Resource Model

Quote adalah aggregate utama CPQ.

8.1 Collection

POST /quotes
GET /quotes

Create quote:

POST /api/v1/quotes
Idempotency-Key: idem_01JZ8105KVWG0XH6GDXXR9N56Q
X-Correlation-Id: corr_01JZ810BQDME1DJ3SS4VDACXEA
{
  "customerId": "cus_01JZ7R6JXXM3B2ZD8EV2B4H6JD",
  "salesChannel": "DIRECT_SALES",
  "currency": "IDR",
  "validityPeriod": {
    "validFrom": "2026-07-02T00:00:00+07:00",
    "validUntil": "2026-07-31T23:59:59+07:00"
  }
}

Response:

201 Created
Location: /api/v1/quotes/quo_01JZ7R7Y95W6PPXN3DWV4K8FQG
{
  "id": "quo_01JZ7R7Y95W6PPXN3DWV4K8FQG",
  "status": "DRAFT",
  "version": 1,
  "customerId": "cus_01JZ7R6JXXM3B2ZD8EV2B4H6JD",
  "salesChannel": "DIRECT_SALES",
  "currency": "IDR",
  "createdAt": "2026-07-02T10:15:30+07:00"
}

8.2 Quote Item

POST /quotes/{quoteId}/items
GET /quotes/{quoteId}/items
GET /quotes/{quoteId}/items/{quoteItemId}
PATCH /quotes/{quoteId}/items/{quoteItemId}

Add item request:

{
  "productOfferingId": "po_01JZ81B5MGDE53NKZTCSEW3VKG",
  "quantity": 1,
  "action": "ADD"
}

Quote item response:

{
  "id": "qit_01JZ81E0661QPRBK1H43X84PE7",
  "quoteId": "quo_01JZ7R7Y95W6PPXN3DWV4K8FQG",
  "productOfferingId": "po_01JZ81B5MGDE53NKZTCSEW3VKG",
  "action": "ADD",
  "quantity": 1,
  "configurationStatus": "REQUIRES_CONFIGURATION",
  "pricingStatus": "NOT_PRICED"
}

8.3 Quote Pricing Run

POST /quotes/{quoteId}/pricing-runs
GET /quotes/{quoteId}/pricing-runs/{pricingRunId}

Pricing run response:

{
  "id": "prr_01JZ81K0K3G3JDBYDKH6NXXT6P",
  "quoteId": "quo_01JZ7R7Y95W6PPXN3DWV4K8FQG",
  "status": "COMPLETED",
  "inputQuoteVersion": 4,
  "priceSnapshotVersion": 2,
  "totalOneTimeCharge": {
    "amount": "250000.00",
    "currency": "IDR"
  },
  "totalRecurringCharge": {
    "amount": "750000.00",
    "currency": "IDR"
  }
}

Pricing run adalah resource karena hasilnya bisa diaudit dan dibandingkan.


9. Quote Lifecycle Commands

Quote lifecycle transition harus eksplisit.

Resource untuk command:

TransitionEndpoint
submitPOST /quotes/{quoteId}/submission-requests
approve/rejectPOST /approval-requests/{approvalRequestId}/decisions
acceptPOST /quotes/{quoteId}/acceptance-requests
convertPOST /quotes/{quoteId}/conversion-requests
cancelPOST /quotes/{quoteId}/cancellation-requests
revisePOST /quotes/{quoteId}/revisions

Kenapa revisions resource?

Karena revision bukan update biasa. Revision menciptakan versi quote baru atau branch baru dengan traceability ke versi sebelumnya.


10. Order Resource Model

Order adalah execution commitment.

10.1 Create Order

Ada dua sumber utama:

  1. quote-derived order;
  2. direct order.

Quote-derived order:

POST /quotes/{quoteId}/conversion-requests

Direct order:

POST /orders

Direct order harus tetap melewati validasi yang kuat.

Request:

{
  "customerId": "cus_01JZ7R6JXXM3B2ZD8EV2B4H6JD",
  "source": "DIRECT_ORDER",
  "items": [
    {
      "productOfferingId": "po_01JZ81B5MGDE53NKZTCSEW3VKG",
      "action": "ADD",
      "quantity": 1,
      "configuration": {
        "characteristics": [
          {
            "name": "bandwidth",
            "value": "1Gbps"
          }
        ]
      }
    }
  ]
}

10.2 Order Read Model

GET /orders/{orderId}

Response should include business status, not workflow internals:

{
  "id": "ord_01JZ82C9PCSDPW5QX234JFN4ZC",
  "status": "IN_PROGRESS",
  "source": "QUOTE",
  "sourceQuoteId": "quo_01JZ7R7Y95W6PPXN3DWV4K8FQG",
  "customerId": "cus_01JZ7R6JXXM3B2ZD8EV2B4H6JD",
  "version": 5,
  "createdAt": "2026-07-02T11:15:30+07:00",
  "submittedAt": "2026-07-02T11:16:00+07:00"
}

Do not expose this in public response:

{
  "zeebeProcessInstanceKey": "2251799813685251",
  "workerType": "reserve-resource-worker",
  "bpmnElementId": "Activity_1x7abc"
}

Camunda details belong in operational/internal view.


11. Order Lifecycle Commands

Resource:

/orders/{orderId}/cancellation-requests
/orders/{orderId}/amendment-requests
/orders/{orderId}/hold-requests
/orders/{orderId}/resume-requests

Cancellation request:

{
  "reasonCode": "CUSTOMER_REQUEST",
  "comment": "Customer requested cancellation before activation.",
  "requestedEffectiveDate": "2026-07-05T00:00:00+07:00"
}

Response:

{
  "id": "ocr_01JZ82SSYFCT8R8WV3D9EZ95RM",
  "orderId": "ord_01JZ82C9PCSDPW5QX234JFN4ZC",
  "status": "ACCEPTED_FOR_PROCESSING",
  "requiresCompensation": true,
  "createdAt": "2026-07-02T11:20:00+07:00"
}

Cancellation is not delete.

Cancellation is a business process.


12. Fulfillment Resource Model

Fulfillment is usually internal/operations.

Resource:

GET /fulfillment-plans/{fulfillmentPlanId}
GET /fulfillment-plans/{fulfillmentPlanId}/tasks
GET /fulfillment-tasks/{fulfillmentTaskId}
POST /fulfillment-tasks/{fulfillmentTaskId}/retry-requests
POST /fulfillment-tasks/{fulfillmentTaskId}/manual-completion-requests

Fulfillment task response:

{
  "id": "fft_01JZ834SMJ3BE0M20M1PHRSVFY",
  "fulfillmentPlanId": "ffp_01JZ833V63S0JACGDFHK77AMK7",
  "orderId": "ord_01JZ82C9PCSDPW5QX234JFN4ZC",
  "orderItemId": "oit_01JZ82D3R9SD50KTG2Y0KEQT3V",
  "taskType": "RESERVE_RESOURCE",
  "status": "FAILED_RETRYABLE",
  "attemptCount": 3,
  "lastFailureCode": "INVENTORY_TIMEOUT",
  "retryable": true
}

Retry request:

POST /fulfillment-tasks/{fulfillmentTaskId}/retry-requests
{
  "reason": "Inventory service recovered.",
  "requestedBy": "usr_01JZ80Q1QQN8A69KJGF3SRBSP4"
}

This command must not call Zeebe blindly.

It must:

  • validate task state;
  • validate retryability;
  • create audit record;
  • update local task state;
  • trigger workflow/message safely;
  • publish event through outbox.

13. Approval Resource Model

Approval is its own domain surface.

GET /approval-requests
GET /approval-requests/{approvalRequestId}
POST /approval-requests/{approvalRequestId}/decisions
POST /approval-requests/{approvalRequestId}/escalations

Decision request:

{
  "decision": "APPROVED",
  "reason": "Discount is acceptable for enterprise renewal.",
  "conditions": [
    {
      "type": "VALID_UNTIL",
      "value": "2026-07-15T23:59:59+07:00"
    }
  ]
}

Decision response:

{
  "id": "apd_01JZ83MJCTN8VT0AK62A33ZTEV",
  "approvalRequestId": "apr_01JZ80R4JK3BGEP66WCR8QVE7Z",
  "decision": "APPROVED",
  "decidedAt": "2026-07-02T12:00:00+07:00",
  "resultingApprovalStatus": "APPROVED"
}

Approval decision is append-only.

Do not update old decision silently.

If correction is needed, create reversal/correction record with audit.


14. Asset and Subscription Resource Model

Asset is installed reality.

Resource:

GET /assets?customerId={customerId}
GET /assets/{assetId}
GET /assets/{assetId}/versions
GET /assets/{assetId}/relationships
GET /subscriptions?customerId={customerId}
GET /subscriptions/{subscriptionId}

Asset response:

{
  "id": "ast_01JZ840DBKSMA4BJCC0J8HB77M",
  "customerId": "cus_01JZ7R6JXXM3B2ZD8EV2B4H6JD",
  "productOfferingId": "po_01JZ81B5MGDE53NKZTCSEW3VKG",
  "status": "ACTIVE",
  "effectiveFrom": "2026-07-02T13:00:00+07:00",
  "sourceOrderId": "ord_01JZ82C9PCSDPW5QX234JFN4ZC",
  "version": 3
}

Mutation rule:

Normal asset change must come from order lifecycle.
Direct asset correction must use controlled repair API.

Why?

Because direct asset update can break:

  • billing;
  • subscription;
  • fulfillment history;
  • customer promise;
  • audit evidence;
  • future CPQ eligibility.

15. Search, Filtering, Sorting, Pagination

Collection endpoints must be predictable.

Example:

GET /quotes?customerId=cus_01JZ7R6JXXM3B2ZD8EV2B4H6JD&status=APPROVED&pageSize=50&pageToken=abc

Response:

{
  "items": [
    {
      "id": "quo_01JZ7R7Y95W6PPXN3DWV4K8FQG",
      "status": "APPROVED",
      "customerId": "cus_01JZ7R6JXXM3B2ZD8EV2B4H6JD",
      "createdAt": "2026-07-02T10:15:30+07:00"
    }
  ],
  "page": {
    "nextPageToken": "eyJvZmZzZXQiOjUwfQ",
    "pageSize": 50
  }
}

Rules:

  • do not expose raw DB offset as stable contract;
  • use stable sort for pagination;
  • define max page size;
  • define default sort;
  • document filter combinations;
  • reject unsupported filters instead of silently ignoring;
  • index query patterns in PostgreSQL.

16. Projection and Expansion

Consumer often wants either summary or full detail.

Do not always return giant aggregate.

Options:

16.1 Dedicated Summary Resource

GET /quotes

returns quote summary.

GET /quotes/{quoteId}

returns quote detail.

16.2 Include Parameter

GET /quotes/{quoteId}?include=items,priceSnapshot,approvalSummary

Rules:

  • include values must be allowlisted;
  • default response must be stable;
  • included objects must not change lifecycle semantics;
  • avoid arbitrary graph expansion.

Bad:

GET /quotes/{id}?include=*

That turns API into uncontrolled object graph serialization.


17. Version and Optimistic Concurrency

For mutating commands, include version when command depends on observed state.

Example:

{
  "expectedQuoteVersion": 5,
  "comment": "Submit after final pricing."
}

If current quote version is 6:

409 Conflict
{
  "code": "QUOTE_VERSION_CONFLICT",
  "message": "Quote version changed after the caller last read it.",
  "currentVersion": 6,
  "expectedVersion": 5
}

Why not rely only on idempotency?

Because idempotency prevents duplicate command execution.

Optimistic concurrency prevents stale decision execution.

Both are needed.


18. Idempotency By Resource Type

Mutating command endpoints need idempotency.

EndpointIdempotency Required?Reason
POST /quotesyesavoid duplicate quote from retry
POST /quotes/{id}/itemsyesavoid duplicate item
POST /quotes/{id}/pricing-runsusually yesavoid duplicate pricing records
POST /quotes/{id}/submission-requestsyesavoid duplicate approval/workflow
POST /approval-requests/{id}/decisionsyesavoid duplicate decision
POST /ordersyesavoid duplicate order
POST /orders/{id}/cancellation-requestsyesavoid duplicate cancellation
GET /orders/{id}nosafe read

Idempotency key scope:

tenant + actor/client + endpoint + idempotency key

Store:

  • request hash;
  • response summary;
  • resource ID created;
  • status;
  • expiry;
  • correlation ID;
  • created time.

19. Sync vs Async Commands

Not every command should complete synchronously.

19.1 Synchronous Command

Good for quick operations:

create draft quote
add quote item
update contact
basic validation

Response can return final result.

19.2 Asynchronous Command

Good for long-running operations:

submit order
decompose order
run external provisioning
generate contract document
perform large pricing simulation
cancel in-progress order

Response should return accepted request resource:

202 Accepted
Location: /api/v1/orders/ord_.../cancellation-requests/ocr_...
{
  "id": "ocr_01JZ82SSYFCT8R8WV3D9EZ95RM",
  "status": "ACCEPTED_FOR_PROCESSING",
  "orderId": "ord_01JZ82C9PCSDPW5QX234JFN4ZC"
}

Don't block HTTP while waiting for external provisioning.


20. Timeline and Events API

Business users often need timeline.

GET /orders/{orderId}/timeline
GET /quotes/{quoteId}/timeline

Timeline is not raw Kafka event stream.

Timeline is curated business view:

{
  "items": [
    {
      "type": "ORDER_CREATED",
      "occurredAt": "2026-07-02T11:15:30+07:00",
      "actor": "usr_01JZ80Q1QQN8A69KJGF3SRBSP4",
      "summary": "Order was created from accepted quote."
    },
    {
      "type": "FULFILLMENT_TASK_FAILED",
      "occurredAt": "2026-07-02T11:45:00+07:00",
      "summary": "Inventory reservation timed out."
    }
  ]
}

Why curated?

Because raw events may include technical noise, internal fields, retries, or sensitive data.


21. Operational Repair APIs

Repair API must be explicit and audited.

Examples:

POST /operations/repair-actions
GET /operations/repair-actions/{repairActionId}
POST /operations/reconciliation-runs

Repair action request:

{
  "targetType": "FULFILLMENT_TASK",
  "targetId": "fft_01JZ834SMJ3BE0M20M1PHRSVFY",
  "actionType": "MARK_EXTERNAL_STEP_COMPLETED",
  "reason": "Provisioning completed externally but callback failed.",
  "evidenceReference": "INC-2026-000187"
}

This is dangerous.

So it must enforce:

  • strong authorization;
  • four-eyes approval for high-risk action;
  • audit before/after;
  • allowed target state;
  • allowed repair type;
  • event emission;
  • reconciliation marker.

Never allow generic:

POST /operations/sql

or:

PATCH /orders/{id}
{
  "status": "COMPLETED"
}

22. API Boundary Against Camunda

API should not expose Camunda as the business API.

Bad:

POST /process-instances
GET /process-instances/{key}
POST /jobs/{jobKey}/complete

Good:

POST /orders/{orderId}/cancellation-requests
GET /orders/{orderId}
GET /fulfillment-plans/{fulfillmentPlanId}
GET /operations/incidents/{incidentId}

Camunda/Zeebe is orchestration infrastructure.

Business API should expose business resource.

Internal operations may show workflow correlation, but even then it should be wrapped in domain language.


23. API Boundary Against Kafka

Kafka event is not query API.

Do not tell consumers:

Read topic order-events to know order status.

For business query:

GET /orders/{orderId}

For integration facts:

order.events.v1 topic

Both exist.

They serve different needs.

API answers current state.

Events publish facts that occurred.


24. API Boundary Against Redis

Redis cache is invisible to API contract.

Do not expose:

DELETE /redis/keys/{key}

Instead expose domain-safe operation:

POST /operations/cache-invalidation-requests

or make invalidation event-driven internally.

If cache must be controlled, resource should be operational and audited.


25. API Boundary Against PostgreSQL/MyBatis

Database and mapper are implementation details.

Do not expose:

GET /orders?sqlWhere=status='FAILED'
GET /quote-items?join=price_component

Expose supported filters:

GET /orders?status=FAILED&createdFrom=2026-07-01T00:00:00+07:00

Query flexibility is good.

Unbounded query language over public API is not.


26. OpenAPI Path Skeleton

A first skeleton:

paths:
  /quotes:
    post:
      operationId: createQuote
    get:
      operationId: searchQuotes

  /quotes/{quoteId}:
    get:
      operationId: getQuote
    patch:
      operationId: patchQuote

  /quotes/{quoteId}/items:
    post:
      operationId: addQuoteItem
    get:
      operationId: listQuoteItems

  /quotes/{quoteId}/items/{quoteItemId}:
    get:
      operationId: getQuoteItem
    patch:
      operationId: patchQuoteItem

  /quotes/{quoteId}/pricing-runs:
    post:
      operationId: createQuotePricingRun
    get:
      operationId: listQuotePricingRuns

  /quotes/{quoteId}/submission-requests:
    post:
      operationId: submitQuote

  /quotes/{quoteId}/acceptance-requests:
    post:
      operationId: acceptQuote

  /quotes/{quoteId}/conversion-requests:
    post:
      operationId: convertQuoteToOrder

  /orders:
    post:
      operationId: createOrder
    get:
      operationId: searchOrders

  /orders/{orderId}:
    get:
      operationId: getOrder

  /orders/{orderId}/cancellation-requests:
    post:
      operationId: requestOrderCancellation

This skeleton is not final implementation.

It is a modelling baseline.


27. JAX-RS Resource Direction

Resource class should align with API grouping.

@Path("/api/v1/quotes")
public class QuoteResource {

    @POST
    public Response createQuote(CreateQuoteRequest request) {
        // map -> application service -> map response
    }

    @GET
    @Path("/{quoteId}")
    public Response getQuote(@PathParam("quoteId") String quoteId) {
        // query application service
    }

    @POST
    @Path("/{quoteId}/submission-requests")
    public Response submitQuote(
        @PathParam("quoteId") String quoteId,
        SubmitQuoteRequest request
    ) {
        // command application service
    }
}

Do not put every endpoint in one giant resource.

Possible grouping:

CatalogResource
ConfigurationResource
PricingResource
QuoteResource
ApprovalResource
OrderResource
FulfillmentResource
AssetResource
OperationalResource

But grouping by URL and capability is better than grouping by database table.


28. Error Modelling Per Resource

Each resource needs known errors.

Example quote submission:

CodeStatusMeaning
QUOTE_NOT_FOUND404quote ID unknown
QUOTE_VERSION_CONFLICT409expected version stale
QUOTE_INVALID_STATE409quote cannot be submitted from current state
QUOTE_EXPIRED409quote validity elapsed
QUOTE_HAS_INVALID_CONFIGURATION422item configuration invalid
QUOTE_NOT_PRICED422pricing snapshot missing
APPROVAL_POLICY_UNAVAILABLE503policy service unavailable

Error model is part of resource design.

If you cannot list expected errors, you do not understand the resource yet.


29. Security Scope Per Resource

Security is not just global login.

Example scopes:

catalog:read
quote:create
quote:read
quote:update
quote:submit
quote:accept
approval:read
approval:decide
order:create
order:read
order:cancel
fulfillment:read
fulfillment:repair
asset:read
operations:repair

Commands need stronger authorization than reads.

Repair APIs need strongest authorization.

Approval decision must check assigned approver/delegation, not just role.


30. Resource Design Review Checklist

For each proposed resource, ask:

Does it have business meaning?
Does it have identity?
Does it have lifecycle?
Who owns it?
Who consumes it?
Is it public, internal, integration, or operational?
Is it read model or command resource?
Is mutation idempotent?
What state transitions are legal?
What events are emitted?
What audit evidence is required?
What error codes are expected?
What fields are stable contract?
Does this leak database/workflow/cache internals?
Does this need pagination?
Does this need optimistic concurrency?

If many answers are unclear, resource modelling is not finished.


31. Anti-Patterns

31.1 Verb Explosion

Bad:

/calculatePrice
/submitQuote
/approveQuote
/rejectQuote
/convertQuote
/startOrder
/stopOrder
/retryTask

Better:

/quotes/{quoteId}/pricing-runs
/quotes/{quoteId}/submission-requests
/approval-requests/{approvalRequestId}/decisions
/quotes/{quoteId}/conversion-requests
/fulfillment-tasks/{fulfillmentTaskId}/retry-requests

31.2 Status Mutation API

Bad:

PATCH /orders/{id}
{
  "status": "COMPLETED"
}

Better:

POST /fulfillment-tasks/{taskId}/manual-completion-requests

or a domain-specific completion command with evidence.

31.3 Exposing Workflow Engine

Bad:

GET /zeebe/process-instances/{key}

Better:

GET /orders/{orderId}/timeline
GET /operations/incidents/{incidentId}

31.4 Infinite Include

Bad:

GET /customers/{id}?include=quotes.orders.assets.billing.provisioning.everything

Better:

GET /customers/{id}/commercial-summary
GET /quotes?customerId=...
GET /orders?customerId=...
GET /assets?customerId=...

31.5 Generic Admin Mutator

Bad:

POST /admin/change-status

Better:

POST /operations/repair-actions

with typed action and guardrails.


32. Minimal API Set For First Build Slice

Untuk implementasi awal, kita tidak akan membuat semua resource sekaligus.

Build slice pertama cukup:

Catalog read API
Create quote
Add quote item
Configure quote item
Run pricing
Submit quote
Approve quote
Accept quote
Convert quote to order
Get order
Track order timeline

Endpoint minimal:

GET  /product-offerings
GET  /product-offerings/{id}
POST /quotes
GET  /quotes/{id}
POST /quotes/{id}/items
PATCH /quotes/{id}/items/{itemId}
POST /quotes/{id}/pricing-runs
POST /quotes/{id}/submission-requests
GET  /approval-requests/{id}
POST /approval-requests/{id}/decisions
POST /quotes/{id}/acceptance-requests
POST /quotes/{id}/conversion-requests
GET  /orders/{id}
GET  /orders/{id}/timeline

Itu sudah cukup untuk membuktikan core CPQ-to-order lifecycle.


33. How This Part Connects To Next Part

Part berikutnya akan masuk ke struktur OpenAPI lebih teknis:

components
request/response envelope
error model
pagination
filtering
sorting
headers
idempotency
correlation ID
problem detail

Di part ini kita memilih resource.

Di part berikutnya kita membuat kontrak teknisnya lebih rapi.


34. Latihan Praktis

Ambil satu resource: OrderCancellationRequest.

Jawab:

Apa parent resource-nya?
Apakah dia punya ID sendiri?
Apakah cancellation sync atau async?
Apa state awal order yang legal?
Apa state task yang legal?
Apa response saat accepted?
Apa response saat rejected?
Apa event yang keluar?
Apa audit evidence wajib?
Apa idempotency key scope?
Apa authorization scope?
Apa error code domain?
Apa field yang immutable?

Jika jawabanmu jelas, kamu sudah mulai berpikir seperti designer enterprise API.


35. Ringkasan

Resource modelling untuk CPQ/OMS harus dimulai dari business capability, bukan database table.

Prinsip yang kita pakai:

Expose business resources.
Model lifecycle transitions as explicit command resources.
Use GET for safe reads.
Use POST for command/resource creation.
Use PATCH only for simple partial mutation, not lifecycle transition.
Avoid DELETE for audited business objects.
Make idempotency explicit.
Separate public business API from internal workflow/operations API.
Never expose Camunda, Kafka, Redis, PostgreSQL, or MyBatis details as public contract.

Tujuan akhirnya sederhana:

A consumer should understand the business system from the API surface without knowing how the service is implemented.

36. Referensi Resmi

Lesson Recap

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