← Blog

Interface Segregation in MuleSoft: an API for each consumer

Code for this article: src/main/mule/isp · contracts: isp-order-views.raml · tests: isp-test-suite.xml

The Interface Segregation Principle says no client should be forced to depend on methods it does not use. Many small interfaces beat one large one, because a client coupled to a large interface is affected by every change to it — including changes to the parts it never touches.

Where it fits in Mule, and where it doesn’t

Inside a Mule application there are no interfaces to segregate. Splitting a flow into sub-flows is Single Responsibility, not ISP; the first version of this article blurred the two.

At the API contract level the fit is excellent. An API specification is exactly the kind of interface Martin was talking about, and consumers — a mobile app, a finance system, a partner — are the clients. In API-led terms, ISP is the reason experience APIs exist.

Before: one endpoint for everyone

<flow name="isp-before-get-order">
    <http:listener config-ref="http-listener-config" path="/isp/before/orders/{orderId}" allowedMethods="GET">
        <http:response statusCode="#[vars.httpStatus default 200]" />
    </http:listener>
    <flow-ref name="isp-load-order" />
</flow>

GET /isp/before/orders/ORD-1001 returns the full record: tracking, customer contact details, address, items, billing — and an internal block with cost price, supplier and margin.

Two problems follow:

  • Coupling. When finance needs a new billing field, the mobile app’s contract changes too. Every consumer is retested for a change that only one of them asked for.
  • Exposure. The mobile app now receives personal data and internal cost information it never displays. It is in logs, caches and device memory anyway. For integrations, this is the strongest argument for ISP — it is a security and data-protection issue before it is a design one.

After: an interface per consumer

<flow name="isp-after-get-mobile-order">
    <http:listener config-ref="http-listener-config" path="/isp/after/mobile/orders/{orderId}" allowedMethods="GET">
        <http:response statusCode="#[vars.httpStatus default 200]" />
    </http:listener>
    <flow-ref name="isp-load-order" />
    <ee:transform>
        <ee:message>
            <ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
    orderId: payload.orderId,
    status: payload.status,
    estimatedDelivery: payload.estimatedDelivery
}]]></ee:set-payload>
        </ee:message>
    </ee:transform>
</flow>

Finance gets its own /isp/after/finance/orders/{orderId} with invoice, total, tax and payment status. The internal cost block is not exposed at all. Each resource carries the {orderId} it refers to — something the first version’s /orders/stock and /orders/payment-status endpoints forgot.

The evidence

The tests assert the exact set of fields each consumer receives. The mobile view is precisely orderId, status, estimatedDelivery; the finance view is precisely its five billing fields. The before test asserts the opposite, on purpose: the customer’s e-mail and the internal cost price are there for everyone.

Don’t overdo it

  • One API per screen does not scale. Segregate by kind of consumer with a different need, not by every page of every app.
  • Consider field selection when consumers are many and similar. Query parameters for field sets, or GraphQL, can give each consumer a narrow view without a new API each time.
  • Keep one system of record behind the views. The example has a single isp-load-order; the views shape data, they do not duplicate its retrieval.

Checklist

  • Does any consumer receive fields it does not use?
  • Would a change requested by one consumer alter another consumer’s contract?
  • Is personal or commercially sensitive data exposed only to the consumers that need it?
  • Are the views shaping one source, rather than each re-implementing the retrieval?