Decide Where an API Boundary Belongs

Use the completed service to reason about ownership, System/Process/Experience responsibilities and the cost of another call.

Mobile, web and a partner portal now need order status. Each could query the database and translate backend status codes independently, but three copies of that knowledge will drift. The application we built gives us a concrete place to ask which responsibility deserves an API boundary.

The expensive duplication is not the database connection. It is the business knowledge copied around it. A network boundary is useful when it contains a reason to change, an ownership rule or a reusable business capability. The cost is another interface somebody must own, together with the latency, deployment work and failure modes that arrive with it. A three-box diagram is not a requirement to deploy three services.

System facts: Backend translations: Stable meanings. Business capability: Combine known facts: Partial-failure policy. Consumer view: Permitted fields: Useful presentation. Add a network boundary when its ownership benefit earns the cost.

Trace a boundary in ACB

Open checkpoint 30-recovery-capstone for this design exercise. Use Flow List to locate create-order, accept-once, the catalogue requester and the dispatcher. Follow each Flow Reference using its configured Name. Draw the proposed service boundary around the responsibility, then identify every variable, payload field and error currently crossing those references.

Open the design artifact in Example 073 in the text editor alongside the canvas. Turning a subflow into a separate service requires an explicit request, response, identity and failure contract; it is not a canvas move. Keep the executable local checkpoint intact while comparing the proposed deployment boundaries.

The mess a boundary should remove

The first direct integration can be perfectly reasonable. One team needs one report, the database contract is stable, and the result has one consumer. Adding three independently deployed APIs before the first requirement is understood can produce more operational work than useful isolation.

The warning sign is knowledge spreading across consumers. The mobile team learns that ST=40 means handed to carrier. The partner team learns that cancelled orders remain in a different table. The web team adds a fallback when the shipping system has no tracking record yet. Soon the system is governed by duplicated interpretations rather than a shared contract.

Direct connections also multiply faster than a team expects. Ten systems wired to each other have 45 possible pairs, 10 × 9 / 2 — an upper bound for an undirected, fully connected network rather than a prediction that any company has exactly that many integrations. The arithmetic matters because every one of those connections carries the same decisions. The CRM calls a customer C-42 while the warehouse knows the same person as ACCOUNT_907. One endpoint reports an absent record with HTTP 404; another returns HTTP 200 containing an error object. Each direct integration learns those differences independently, and each one learns them slightly differently.

Putting a box marked “middleware” between the systems does not by itself fix that. If the box forwards every private schema and exception, its consumers remain coupled to the database behind it, and you have added a hop without moving a decision. The useful boundary is a contract with an owner — what requests are accepted, what responses mean, and which changes preserve compatibility. Reuse reduces the number of places that must understand a system; it does not remove the system’s complexity.

An Orders System API can own the translation from database records to order facts. An Order Status Process API can combine those facts with shipment information. An experience-specific interface can decide which fields a particular caller receives. Those are different reasons to change, even if an early implementation keeps some responsibilities in one deployable application.

Boundaries belong where ownership and behavior actually divide. Creating an API called orders-process-api does not make its contract reusable if every field is named after a mobile screen. Conversely, a direct call to a stable managed backend API is not automatically an architectural failure — MuleSoft’s own simplification guidance allows reducing layers when a separate interface adds no useful responsibility. Simplifying accelerator assets.

Three layers, three jobs

A System API protects consumers from the details of a backend. It can translate identifiers, expose supported operations, normalize source-specific errors and enforce access to that system. It should not promise that every backend replacement is invisible — it promises an interface whose compatibility the owner can test.

A Process API expresses a reusable capability. An order-status capability might reconcile order and shipping facts into a coherent view. An order-fulfillment capability might coordinate inventory, payment and dispatch. It owns the policy that crosses systems, rather than the presentation of one screen.

An Experience API adapts a capability for a consumer or channel. The mobile response might omit a large event history. A partner response might expose only permitted fields. The experience boundary is useful when the consumer’s pace of change, access model or payload needs justify it — these are architectural responsibilities, not Mule processor types. MuleSoft’s API-led model.

The reference model does not require every request to traverse exactly three network hops. A consumer can use a suitable System API directly, or a Process API can serve multiple consumers without another adapter. What matters is whether its contract already meets those consumers’ needs without mixing incompatible responsibilities. API-led integration patterns.

The knowledge owned by each layer gives you a practical review test: the Orders System API knows the source’s status mapping, the Process API knows how to handle an order without a shipment, and the mobile adapter knows which labels and fields the app needs. If two layers both determine whether an order may be cancelled, that rule has no clear owner.

Keep status reading distinct from order acceptance

The running service accepts A-1001 and can retrieve its recorded acceptance. It has no carrier integration or shipped-state lifecycle. The following design fixture uses another identity so a proposed status capability cannot be mistaken for observed output from the acceptance checkpoint.

Example 073 — Design a partial order-status response

Source: workshops/architecture/status-view.json.

{
  "orderId": "STATUS-2001",
  "tenantId": "retailer-a",
  "orderStatus": "SHIPPED",
  "shipment": null,
  "shipmentAvailable": false
}

STATUS-2001 is a hypothetical already shipped order whose carrier lookup is unavailable. The Process API retains the known order fact and marks the unavailable shipment explicitly. An informational screen can explain that absence — a service selling a guaranteed delivery date may instead need to fail, because the missing fact is required for its decision.

A missing order is a different case, and the contract has to keep it different. Returning an empty order with shipmentAvailable: false would confuse “the order does not exist” with “the carrier is down,” and no consumer can separate those two from the fields alone. A shared API earns reuse only if consumers can interpret its failures as reliably as its successes.

A separate API requires a real call

The flow-ref in chapter 5 only invokes another flow inside the same Mule application — naming a subflow orders-process-api does not grant it independent deployment or a separate owner. A capability that really is deployed on its own needs a network contract, authentication, a timeout and an error mapping, and each of those is a decision the drawing does not make for you.

Capture required identifiers before an HTTP Request replaces the message, use the requester’s URI-parameter map, and adapt the returned contract rather than reaching through it for a backend-specific column. If the transform on the far side needs payload.legacy_status_code, the boundary is leaking: that transform should adapt the Process API’s contract instead of inspecting its database-shaped internals. The target and attribute lessons in chapters 9–10 apply unchanged.

Time budgets cross layers

An extra API hop consumes time even when its code is small. Suppose the screen has an illustrative end-to-end budget of two seconds. Giving the Experience API, Process API and both dependencies a two-second timeout does not create a two-second journey. Sequential waits can add up, and retries can spend the budget again.

Instead, allocate the budget through the call graph: reserve time for local processing and response transmission, then give downstream calls a smaller remaining window. A dependency needed only for an optional shipment section should not occupy the entire order-view budget. The precise numbers require measurement against real traffic and failure behavior; the example budget is a design exercise.

Independent reads can sometimes run concurrently. That reduces the sum of waits toward the slower wait, but it introduces a join policy — what happens if one result is missing or failed? Parallelizing the calls without designing that policy merely moves the ambiguity into a Scatter-Gather error handler.

If the Experience API retries the Process API while the Process API retries the carrier, one customer action can multiply outbound requests — and during a dependency outage that amplification is what makes recovery harder. Give retries one clear owner at the point where the operation’s safety and remaining time are understood, and record enough context to distinguish original attempts from retries.

Ownership is part of the interface

A reusable Orders API needs an owner who can answer a compatibility question and an operational question. Who approves a new status value? Who responds when the API meets its own response time but returns stale data? Who announces a planned source migration? Write those responsibilities beside the contract.

A catalog entry with no owner, no support path and no version policy is discoverable code, not a dependable service. Exchange can publish a versioned asset, but the team still has to define the meaning and support expectations of that version.

Version the API contract separately from the deployment artifact. A patch to logging can change the JAR without changing the API’s major version. Removing currency, renaming orderId or changing a field from number to string can break callers even if the endpoint path remains /v1. Compatibility is about observable behavior, not the size of the code diff.

Adding a response enum value is a particularly useful review case. The provider sees an additive change; a consumer with an exhaustive switch sees a new branch it cannot handle. Decide whether the contract promises a closed set or allows unknown values, and give consumers an example of the intended fallback. Independent deployment requires an agreement about such changes.

Reuse can move the bottleneck

A shared Process API centralizes useful behavior, but it also concentrates traffic. Imagine the mobile app requests status every few seconds while a partner launches a bulk reconciliation. Both now compete for the same downstream capacity. Reuse has increased the importance of admission control, quotas and fair resource use.

Separate interactive and bulk workloads when their budgets differ. The status screen needs a prompt answer; the partner import may accept an asynchronous job and progress report. Making both use the same synchronous endpoint can let a large batch consume connections needed by customers.

A cache shared across customers without the relevant access context can expose another customer’s data. So caching needs an owner too, and a cached order-status result must identify the freshness it promises and respect the caller’s authorization context. Even a correctly isolated cache can produce business errors if a cancelled order remains visible as shippable beyond the agreed freshness window.

A canonical model can help several consumers agree, but an enterprise-wide object with hundreds of optional fields often couples everyone to everyone else. Prefer a bounded order-status contract whose fields have clear meanings. Add a separate capability when a requirement changes the meaning of the resource, rather than turning every field into an optional escape hatch.

Layering does not grant authorization

An Experience API may authenticate a mobile customer, but the Process API still needs an intentional trust model. An incoming customerId query parameter is not proof of customer identity. A System API exposed to other internal applications cannot assume every caller has performed the same authorization check.

Carry authenticated context through a controlled mechanism, validate caller identity at each exposed boundary as required, and decide which service enforces order ownership. Gateway policies can enforce transport-level access rules, while application logic decides whether the authenticated caller may read A-1001. Keeping those decisions distinct prevents a valid client credential from becoming permission to read every customer’s order.

Similarly, “internal” is a deployment classification, not a complete security design. An API may be reachable only through a private network and still require per-client authorization, quotas and audit evidence. The identity and governance chapters covered those implementation choices — the architecture should already name who is trusted to assert what.

Test the boundaries before multiplying them

Each layer’s test has its own job. A contract test for the Orders System API should keep passing when the database adapter changes. A Process API test arranges order and shipment facts, then asserts their combination and partial-failure policy. An Experience API test checks the permitted fields and consumer-specific shape.

The three suites still leave a gap. If every layer mocks the layer below with a fixture copied months ago, all three suites can pass while the deployed chain fails. Add a small integration test that uses the actual published contract version, and make contract examples part of provider validation. Mocks accelerate development; they do not prove agreement indefinitely.

For a backend migration, compare old and new adapters on representative cases: absent order, cancelled order, multiple shipments, unknown status and a total with its currency. Investigate differences before switching traffic. A field-by-field comparison is useful, but compare meanings too — such as the moment an order becomes cancellable, because a stable JSON shape can hide a changed business rule.

The architecture is ready to expand when these boundaries make a real change cheaper. Start with the narrowest capability whose ownership and consumers you understand, then add another deployment boundary when its benefits outweigh its latency, operating cost and coordination work.

Try it

1. Two consumers, one contract. Must mobile and web have separate Experience APIs when their data, permissions and release needs match?

Show answer

No. Keep one owned contract if it serves both coherently. Add a boundary when independent presentation, access or change requirements justify its network and operating cost.

2. Carrier unavailable. When is Partial order-status response acceptable, and when is it unsafe?

Show answer

It can support an informational screen that explicitly shows unavailable tracking. It is unsafe as evidence for a guaranteed delivery promise whose required carrier fact is missing.

3. Same fields, changed meaning. A backend changes SHIPPED from handed-to-carrier to label-printed. Which test should detect the break?

Show answer

A System API semantic contract/migration test using both lifecycle states. Schema equality cannot establish equal meaning. Translate the new backend value or deliberately version the changed promise.

Next: Put a Gateway in the Request Path

Comments