API Contract and DTO Boundary Model
Model API contract dan DTO boundary untuk enterprise CPQ/Quote/Order/Billing systems, termasuk REST/JAX-RS resource model, request/response DTO, command DTO, read DTO, versioning, backward compatibility, validation, error model, pagination, filtering, idempotency key, correlation ID, and production correctness.
API Contract and DTO Boundary Model
1. Core Idea
API contract adalah bentuk publik/eksternal dari domain data model.
Dalam enterprise CPQ / Quote / Order / Billing / Telco BSS/OSS, data model tidak hanya hidup di database. Ia juga muncul sebagai:
- REST resource,
- JAX-RS endpoint,
- request DTO,
- response DTO,
- command DTO,
- event payload,
- integration payload,
- error response,
- pagination/filtering contract,
- validation error,
- OpenAPI schema,
- client SDK model.
Mental model:
Database model is not API model. Entity is not DTO. Internal domain model is not external contract.
API contract harus stabil, aman, versioned, backward-compatible, dan tidak membocorkan detail internal yang tidak boleh menjadi kontrak.
2. Why API Contract Modelling Matters
API modelling yang lemah menyebabkan masalah production:
- client rusak karena field diubah/rename,
- internal enum bocor ke external API,
- database schema change menjadi breaking API change,
- entity JPA/MyBatis langsung diserialisasi,
- sensitive field seperti margin/cost muncul di response,
- PATCH update melewati lifecycle command,
- generic DTO membolehkan illegal state change,
- error response tidak bisa diproses client,
- pagination tidak stabil,
- filter query lambat karena tidak align dengan index,
- idempotency key tidak dipakai untuk create/convert,
- correlation ID hilang dari request chain.
API contract adalah boundary. Boundary yang buruk membuat internal model menjadi sulit berubah dan rentan salah.
3. Entity vs Domain Model vs DTO
| Layer | Purpose |
|---|---|
| Database table | Persistence and query optimization. |
| Entity/record | Persistence or domain representation. |
| Domain model | Business behavior and invariant. |
| Request DTO | External input contract. |
| Response DTO | External output contract. |
| Command DTO | Semantic business action request. |
| Read model DTO | Query/projection optimized output. |
| Event payload | Async integration contract. |
Bad pattern:
@GET
@Path("/quotes/{id}")
public QuoteEntity getQuote(...) {
return quoteRepository.find(id);
}
Risks:
- exposes internal fields,
- lazy-loading serialization bug,
- circular references,
- sensitive fields leak,
- database change breaks API,
- lifecycle invariants bypassed by generic updates.
Better:
QuoteEntity -> QuoteDomain -> QuoteResponseDto
CreateQuoteRequestDto -> CreateQuoteCommand -> QuoteDomain
4. API Resource Model
REST resources should align with domain concepts, not table names blindly.
Examples:
/quotes
/quotes/{quoteId}
/quotes/{quoteId}/items
/quotes/{quoteId}/submit-for-approval
/quotes/{quoteId}/accept
/quotes/{quoteId}/convert-to-order
/orders
/orders/{orderId}
/orders/{orderId}/items
/orders/{orderId}/cancel
/orders/{orderId}/amendments
/billing-accounts/{billingAccountId}
/product-instances/{productInstanceId}
/approval-requests/{approvalRequestId}
Endpoint should express business operation.
Avoid:
PATCH /quote-table/{id}
PATCH /status-column/{id}
POST /execute?action=approve
Unless there is a strong internal reason.
5. Command-Oriented APIs
Lifecycle operations should be command APIs, not generic patch.
Examples:
POST /quotes/{id}/submit-for-approval
POST /quotes/{id}/approve
POST /quotes/{id}/accept
POST /quotes/{id}/cancel
POST /quotes/{id}/convert-to-order
Command request example:
{
"reasonCode": "CUSTOMER_ACCEPTED",
"comment": "Signed acceptance received",
"idempotencyKey": "accept-Q-100-v4-001"
}
Why command API is safer:
- explicit intent,
- specific validation,
- authorization per command,
- audit reason required,
- lifecycle transition controlled,
- side effect/event controlled,
- easier idempotency,
- clearer documentation.
6. Request DTO Design
Request DTO should contain only fields client is allowed to provide.
Example create quote request:
{
"customerId": "customer-id",
"accountId": "account-id",
"billingAccountId": "billing-account-id",
"currency": "USD",
"channel": "SALES_PORTAL",
"items": [
{
"productOfferingId": "offering-id",
"quantity": 1,
"siteId": "site-id",
"configuration": {
"bandwidth": "500Mbps"
}
}
]
}
Do not allow client to set:
- internal ID,
- quote number if system-generated,
- approval status,
- lifecycle status,
- total amount if calculated,
- created_by,
- margin,
- audit fields,
- internal workflow ID,
- converted order ID.
Request DTO is an allowlist, not a mirror of entity.
7. Response DTO Design
Response DTO should be stable and consumer-friendly.
Example:
{
"id": "quote-id",
"quoteNumber": "Q-10001",
"version": 4,
"status": "PRICED",
"customer": {
"id": "customer-id",
"name": "GlobalCorp"
},
"accountId": "account-id",
"currency": "USD",
"totalAmount": "500000.00",
"validUntil": "2026-08-01T00:00:00Z",
"links": {
"self": "/quotes/quote-id",
"items": "/quotes/quote-id/items"
}
}
Response DTO may include:
- stable identifiers,
- display values,
- summary values,
- status,
- version,
- links,
- current allowed actions if useful.
It should not automatically include every internal child collection.
8. Command DTO vs Update DTO
A generic update DTO often creates bugs.
Bad:
{
"status": "APPROVED",
"discountPercent": 25,
"validUntil": "2026-12-31"
}
This mixes multiple commands:
- approve quote,
- change discount,
- extend validity.
Better:
ApplyDiscountCommand
ApproveQuoteCommand
ExtendQuoteValidityCommand
Each command has:
- specific validation,
- specific permission,
- specific audit reason,
- specific side effects,
- specific reapproval rules.
9. Read DTO vs Write DTO
Read and write DTOs should often differ.
Read DTO:
{
"quoteNumber": "Q-10001",
"status": "APPROVED",
"totalAmount": "500000.00",
"approvalSummary": {
"status": "APPROVED",
"approvedAt": "2026-07-12T10:00:00Z"
}
}
Write DTO:
{
"reasonCode": "CUSTOMER_REQUEST",
"comment": "Please cancel this quote"
}
Do not reuse response DTO as update DTO.
Response contains derived/read-only fields. Write DTO should contain command input only.
10. DTO Versioning
APIs evolve.
Common strategies:
| Strategy | Example |
|---|---|
| URL versioning | /v1/quotes |
| Header versioning | Accept: application/vnd.company.quote.v1+json |
| Additive fields | Add optional response fields. |
| New endpoint | Create new contract for breaking change. |
| Field deprecation | Keep old field while adding new field. |
Backward-compatible changes:
- add optional response field,
- add optional request field with default,
- add enum value if clients tolerate unknown,
- add new endpoint.
Breaking changes:
- remove field,
- rename field,
- change type,
- change meaning,
- make optional field required,
- change enum semantics,
- change pagination contract,
- change error shape.
11. Enum Contract
Internal enum and external enum may differ.
Example internal:
QUOTE_STATUS_APPROVAL_PENDING_INTERNAL_REVIEW
External:
PENDING_APPROVAL
External enum should be stable and documented.
Clients must be prepared for unknown enum values if possible.
Do not expose temporary/internal states unless clients need them.
Example:
{
"status": "IN_PROGRESS",
"statusReason": "WAITING_FOR_APPROVAL"
}
This can be more stable than dozens of status enum values.
12. Money and Decimal DTO
Money must be modelled carefully.
Avoid floating point.
Use string decimal or exact decimal representation:
{
"amount": "1234.56",
"currency": "USD"
}
Do not use:
{
"amount": 1234.559999999
}
Money DTO should include:
- amount,
- currency,
- scale/precision convention if needed,
- tax included/excluded semantics,
- period if recurring/usage.
13. Time and Date DTO
Time fields must be explicit.
Use ISO-8601.
Examples:
{
"createdAt": "2026-07-12T10:00:00Z",
"validUntil": "2026-08-01T00:00:00Z",
"billingStartDate": "2026-08-01"
}
Distinguish:
- timestamp/instant,
- local date,
- business period,
- timezone,
- effective date.
Do not serialize local server time without timezone.
14. Validation Error Model
Validation errors should be structured.
Example:
{
"errorCode": "VALIDATION_FAILED",
"message": "Request validation failed",
"correlationId": "corr-123",
"violations": [
{
"field": "items[0].siteId",
"code": "REQUIRED",
"message": "siteId is required for this product offering"
},
{
"field": "billingAccountId",
"code": "BILLING_ACCOUNT_INACTIVE",
"message": "Billing account is not active"
}
]
}
Client should not parse free-text error message.
15. Business Error Model
Business errors differ from validation errors.
Examples:
- illegal state transition,
- quote expired,
- approval required,
- insufficient authority,
- idempotency conflict,
- product not serviceable,
- billing account on credit hold,
- price changed/reprice required,
- order already converted.
Business error response:
{
"errorCode": "QUOTE_APPROVAL_REQUIRED",
"message": "Quote requires approval before acceptance",
"correlationId": "corr-123",
"details": {
"quoteId": "quote-id",
"requiredApprovalType": "DISCOUNT_APPROVAL"
}
}
Use stable error codes.
16. HTTP Status Strategy
Common mapping:
| HTTP status | Use |
|---|---|
| 200 | Successful read/update response. |
| 201 | Created resource. |
| 202 | Accepted asynchronous command. |
| 204 | Successful command with no body. |
| 400 | Malformed/invalid request. |
| 401 | Not authenticated. |
| 403 | Authenticated but not allowed. |
| 404 | Resource not found or not visible. |
| 409 | State conflict/idempotency conflict/concurrent modification. |
| 422 | Business validation failed if used by API standard. |
| 429 | Rate limit. |
| 500 | Unexpected server error. |
| 503 | Service unavailable. |
Define strategy consistently across services.
17. Pagination Model
For list endpoints:
GET /quotes?customerId=...&status=APPROVED&pageSize=50&pageToken=...
Response:
{
"items": [],
"nextPageToken": "token",
"hasMore": true
}
Offset pagination can be unstable for high-change data. Cursor/token pagination is often safer.
Pagination contract should define:
- max page size,
- default page size,
- sort order,
- stable cursor,
- filter semantics,
- total count availability.
18. Filtering and Sorting
Filtering must align with indexed query patterns.
Bad:
GET /orders?filter=anyFieldContainsAnything
This invites slow queries and inconsistent semantics.
Better:
GET /orders?customerId=...&status=IN_PROGRESS&updatedAfter=...
GET /product-instances?billingAccountId=...&status=ACTIVE
For complex search, use dedicated search/read model.
Do not let public API expose arbitrary SQL-like filters unless intentionally designed.
19. Partial Update and PATCH
PATCH is dangerous if it allows arbitrary field mutation.
Safe PATCH needs:
- field allowlist,
- optimistic locking,
- validation,
- audit,
- reason for sensitive fields,
- material change detection,
- permission per field/action.
For lifecycle or commercial changes, prefer command endpoints.
Example acceptable PATCH:
PATCH /quote-drafts/{id}
{
"displayName": "Enterprise Renewal Quote"
}
Example risky PATCH:
PATCH /quotes/{id}
{
"status": "APPROVED",
"totalAmount": "1.00"
}
20. Optimistic Locking in API
For mutable resources, expose version/ETag.
Example:
GET /quotes/{id}
ETag: "quote-v4"
PUT /quotes/{id}
If-Match: "quote-v4"
Or request field:
{
"expectedVersion": 4,
"changes": {}
}
If version mismatch:
409 Conflict
This prevents lost updates.
21. Idempotency Key
Create and command endpoints should support idempotency where duplicate request is harmful.
Examples:
- create quote from external request,
- accept quote,
- convert quote to order,
- create order,
- create billing adjustment,
- trigger billing charge,
- submit fulfillment request.
Header:
Idempotency-Key: quote-123-v4-convert-001
Store:
idempotency_key
request_hash
response_reference
status
created_at
expires_at
If same key with different payload arrives, return conflict.
22. Correlation ID
Every API should accept/return correlation ID.
Header:
X-Correlation-Id: corr-123
Use it in:
- audit,
- logs,
- outbox events,
- workflow,
- integration messages,
- error responses.
If client does not provide one, generate it.
Correlation ID is essential for incident debugging.
23. Sensitive Field Exposure
Response DTO must enforce field-level access.
Sensitive fields:
- margin,
- cost,
- internal discount threshold,
- credit score/status,
- payment method details,
- tax identifiers,
- billing address,
- service/resource identifiers,
- internal workflow IDs,
- raw payload references.
Use projection per role:
QuotePublicDto
QuoteSalesDto
QuoteDealDeskDto
QuoteFinanceDto
Or dynamic field filtering with strong tests.
24. OpenAPI and Schema Governance
OpenAPI should document:
- endpoints,
- request/response DTOs,
- enums,
- error codes,
- pagination,
- headers,
- idempotency key,
- correlation ID,
- examples,
- deprecation notices,
- security scheme.
But OpenAPI generated from code is not enough if semantics are unclear.
Document business meaning:
validUntil is exclusive timestamp.
status is external lifecycle status.
totalAmount excludes tax unless taxIncluded = true.
25. JAX-RS Implementation Considerations
In Java/JAX-RS:
- keep resource classes thin,
- map DTO to command,
- validate DTO at boundary,
- call application service,
- map domain/read model to response DTO,
- handle exceptions centrally,
- add correlation ID filter,
- avoid exposing entity directly.
Structure:
QuoteResource
-> QuoteMapper
-> QuoteCommandService
-> QuoteQueryService
-> ErrorMapper
Example:
@POST
@Path("/{quoteId}/convert-to-order")
public Response convertToOrder(
@PathParam("quoteId") UUID quoteId,
@HeaderParam("Idempotency-Key") String idempotencyKey,
ConvertQuoteRequest request
) {
ConvertQuoteCommand command = mapper.toCommand(quoteId, idempotencyKey, request);
ConvertQuoteResult result = service.convert(command);
return Response.accepted(mapper.toResponse(result)).build();
}
26. DTO Mapping Anti-Patterns
Avoid:
- entity returned directly,
- entity accepted as request body,
- Map<String,Object> everywhere,
- generic update endpoint for lifecycle changes,
- exposing DB IDs without external reference strategy,
- leaking internal enum,
- response DTO with lazy-loaded cycles,
- DTO containing both input and derived output fields,
- silently ignoring unknown fields without policy,
- changing field meaning without versioning.
27. Data Model for API Request Tracking
For critical APIs:
api_request_audit
- id
- method
- path_template
- actor_id
- client_id
- idempotency_key
- correlation_id
- request_hash
- response_status
- error_code
- started_at
- completed_at
Do not store full payload if sensitive. Store hash/reference.
This helps:
- client dispute,
- retry debugging,
- duplicate command analysis,
- incident review.
28. API Contract and Event Contract Alignment
Synchronous API and async event should align semantically.
Example:
POST /quotes/{id}/accept
should produce:
QuoteAccepted event
But response DTO and event payload do not need to be identical.
API response serves caller. Event serves subscribers.
Both should share stable domain meaning.
29. Testing API Contracts
Contract tests should cover:
- required fields,
- optional fields,
- unknown enum behavior,
- error response shape,
- idempotency behavior,
- optimistic lock conflict,
- permission failure,
- validation failure,
- backward compatibility,
- OpenAPI compatibility,
- serialization of money/date/time.
For enterprise APIs, test more than happy path.
30. Failure Modes
| Failure mode | Symptom | Likely cause | Prevention |
|---|---|---|---|
| Client breaks after deploy | Field removed/renamed | No API versioning | Backward compatibility governance |
| Sensitive data leak | Margin/payment exposed | Entity serialized directly | DTO allowlist/field permissions |
| Illegal transition | Status patched directly | Generic update endpoint | Command API |
| Duplicate order | Client retries convert | No idempotency key | Idempotency store |
| Lost update | Two users edit quote | No optimistic locking | ETag/version |
| Slow list API | Arbitrary filters | Query not aligned to index | Governed filters/read model |
| Client cannot handle errors | Free-text error | No error code model | Structured error response |
| Wrong money precision | Rounding mismatch | Floating point JSON/Java double | Decimal/string money model |
| Timezone dispute | Validity off by one day | Ambiguous date/time | ISO/timezone convention |
| API becomes DB contract | Schema change breaks API | Entity-as-DTO | Mapping layer |
31. PR Review Checklist
When reviewing API/DTO changes, ask:
- Is this request DTO, response DTO, command DTO, or event payload?
- Does it expose internal entity fields?
- Are sensitive fields protected?
- Is the endpoint command-oriented where needed?
- Does it require idempotency?
- Does it require optimistic locking?
- Is error response stable and structured?
- Are money/date/time fields precise?
- Are enum values external and documented?
- Is pagination/filtering stable and indexed?
- Is correlation ID propagated?
- Does this change break existing clients?
- Is OpenAPI updated?
- Are contract tests updated?
- Are permission/authority checks server-side?
- Are audit fields generated by system, not client?
32. Internal Verification Checklist
Verify these in the internal CSG/team context:
- API versioning standard.
- JAX-RS resource conventions.
- DTO mapping framework or manual mapper style.
- Whether entities are ever exposed directly.
- Standard error response format.
- Standard correlation ID header.
- Standard idempotency key header/behavior.
- Optimistic locking strategy.
- Pagination/filtering conventions.
- OpenAPI generation/publishing process.
- Field-level permission strategy.
- Sensitive fields in quote/order/billing APIs.
- API compatibility testing process.
- Whether external API differs from internal service API.
- Whether TM Forum APIs are used/adapted and how extensions are handled.
- Whether incidents mention breaking API change, leaked field, duplicate create, or illegal status patch.
33. Summary
API contract and DTO boundary model protect service boundaries.
A strong API data model must define:
- resource model,
- command endpoints,
- request DTO,
- response DTO,
- read DTO,
- DTO/entity separation,
- versioning,
- enum strategy,
- money/time formats,
- error model,
- pagination/filtering,
- idempotency,
- optimistic locking,
- correlation ID,
- sensitive field protection,
- OpenAPI/schema governance,
- contract testing.
The key principle:
Do not let internal data structures become public contracts. API DTOs are boundary models that must preserve business semantics while protecting internal flexibility and production correctness.
You just completed lesson 47 in deepen practice. 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.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.