Microservices Are Not Architectural Boundaries
A review of Chapter 27, “Services: Great and Small,” from Robert C. Martin’s Clean Architecture
Microservices are often presented as a shortcut to a good architecture. Put each capability behind an API, deploy it independently, give it to a small team, and the system will supposedly become decoupled.
Chapter 27 challenges that assumption. A network boundary can separate processes, but it does not automatically separate reasons to change. Services can still be coupled through the data they exchange, the contracts they share, and business features that require several of them to change together.
The chapter’s central lesson is simple:
A deployment boundary is not necessarily an architectural boundary.
The original taxi system
Consider a taxi-ordering system composed of four services:
- `TaxiUI` accepts a customer’s request.
- `TaxiFinder` finds taxis that could serve the request.
- `TaxiSelector` chooses the best candidate according to the customer’s preferences.
- `TaxiDispatcher` places the order with the selected taxi provider.
The request moves through the system in stages. The UI captures it, the finder produces candidates, the selector chooses one, and the dispatcher completes the booking.
At first glance, the services look decoupled. They run separately, communicate through interfaces, and may even be developed and deployed by different teams. However, those facts tell us only about their physical separation. They do not prove that the services can evolve independently.
Two attractive but incomplete promises
“Services are decoupled”
Services do not share function calls or memory in the same way that classes inside one process do, but they still share knowledge. For example, `TaxiFinder` and `TaxiSelector` must agree on the meaning and shape of a candidate taxi. If a new field becomes important to selection, both services may need to understand it. The coupling has not disappeared; it has moved into messages, schemas, APIs, and behavioral expectations. Network calls can therefore be thought of as expensive calls between cooperating parts of the same system. Distance changes the mechanism of communication, not necessarily the degree of business coupling.
“Services can be developed and deployed independently”
Separate repositories and deployment pipelines make independent delivery possible, but only when the change itself respects the service boundaries. A feature that changes one isolated capability may require only one service to be deployed. A feature that cuts across the workflow can still require coordinated work across several teams and services. Cross-cutting behavior exposes the difference between physical independence and independence from change.
The kitten-delivery problem
Now imagine that the business adds kitten delivery. Customers should be able to request a taxi to collect a kitten from a pickup point and deliver it to an address.
The new feature adds several rules:
- The UI must let the customer request kitten delivery.
- The finder must consider only providers and drivers willing to transport kittens.
- The selector must account for allergies. For example, a taxi recently used for kitten delivery should not be selected for an allergic passenger.
- The dispatcher must create the correct kind of order with a participating provider.
The feature follows the customer’s journey across the whole system. It does not fit neatly inside one of the existing service boxes.
If each service is implemented as a small monolith, almost every service must be edited. The teams must agree on new data and behavior, release their changes in a compatible order, and coordinate deployment. The services remain physically separate, but the feature reveals that they are architecturally coupled. This does not prove that microservices are useless. It proves that service boundaries alone do not protect a system from cross-cutting change.
The component-based alternative
Martin’s proposed design introduces abstractions for the stable workflow:
- TaxiFinder
- TaxiSelector
- TaxiDispatcher
- TaxiSupplier
Feature-specific implementations sit behind those abstractions. The ordinary ride behavior is implemented by classes such as `RideFinder` and `RideSelector`; kitten delivery is added through classes such as `KittenFinder` and `KittenSelector`.
The stable policy knows the abstractions, while the volatile feature implementations depend on those abstractions. Composition code—factories controlled from the application’s outer edge—selects and constructs the implementations. This is the Dependency Rule in practice: source-code dependencies point toward stable policy, not outward toward a particular feature. Adding kitten delivery then means adding implementations instead of editing the stable workflow. The UI and composition root still need to expose and assemble the feature, but the existing ride components can remain unchanged. This is the architectural boundary that matters: the boundary between stable policy and volatile feature detail.
Boundaries can run through services
It is tempting to draw every architectural boundary between two microservices. Chapter 27 argues that this picture is too simple. A service can contain several components separated by meaningful dependency boundaries. Conversely, two services can be so tightly coupled by contracts and behavior that the network between them has little architectural significance.
In the taxi example, each functional service can have an internal component structure:
- the finder service contains a stable finder abstraction plus ride-finding and kitten-finding implementations;
- the selector service contains a stable selector abstraction plus ride-selection and kitten-selection implementations;
- the dispatcher and supplier services follow the same pattern.
Viewed vertically, we see independently deployed services such as Finder and Selector. Viewed horizontally, we see business features such as Rides and Kittens passing through those services. The two views describe different kinds of boundaries.
So what exactly are the `Rides` and `Kittens` components?
This was the part I initially found unclear. Should `RideFinder` and `RideSelector` be separate components owned by separate service teams? Or should every service reference one shared `Kittens` component? The most useful answer is: treat Rides and Kittens as logical feature components, but do not assume that they must be one shared deployable library. In a single-process, component-based application, one `Kittens` package could contain all kitten-specific implementations and be loaded by the composition root. In a genuine microservice system, Finder and Selector are separate processes. Each service therefore needs the feature implementation that belongs inside its own boundary:
Finder service
├── TaxiFinder abstraction
├── RideFinder implementation
└── KittenFinder implementation
Selector service
├── TaxiSelector abstraction
├── RideSelector implementation
└── KittenSelector implementation
Those implementations are logically part of the same kitten-delivery feature, but they are normally packaged and deployed with the services in which they execute. A single shared “Kitty” library referenced by every service is possible, but it can recreate tight coupling: every consumer becomes dependent on the same release, data model, and implementation decisions.
The chapter does not require one team per concrete class, nor does it settle team ownership. Team boundaries are an organizational choice. A feature team could own the kitten behavior across several services; service teams could own the extension points within their services; or the teams could collaborate. What matters architecturally is the direction of dependencies and whether adding the feature forces existing stable components to change.
If I want to keep the microservices
The component idea still applies. Each service should have an internal architecture rather than being treated as an indivisible box.
A practical interpretation is:
- Define stable extension points inside each service.
- Keep feature-specific rules behind those interfaces.
- Let the service’s composition root choose the implementations.
- Package each implementation with the service that runs it.
- Version messages and APIs so old and new deployments can coexist during rollout.
- Avoid a large shared feature library unless the shared release coupling is deliberate.
This approach reduces source-code coupling, but it does not magically eliminate operational coordination. If kitten delivery changes the messages exchanged between services, the rollout still needs compatibility planning. Internal components solve one class of coupling; tolerant contracts, versioning, and deployment strategy solve another.
What I took away from the chapter
Microservices are a deployment strategy and a scaling tool. They can improve team autonomy, fault isolation, and operational scaling, but none of those benefits automatically creates clean architecture. Architecture is revealed by change. To find the real boundaries, ask:
- Which parts change for the same business reason?
- Which policies should remain stable while features vary?
- In which direction do source-code dependencies point?
- Can a feature be added without modifying unrelated, stable code?
- Can services accept compatible old and new messages during deployment?
The taxi system’s network diagram shows where processes run. Its component diagram shows how the software resists change. Both views matter, but they answer different questions.
That is the point I was missing at first: boundaries do not have to fall between services. They can—and often should—run through them.