SOFTWARE ARCHITECTURE / MICROSERVICES
Designing Microservices That Don’t Become a Distributed Monolith
A practical way to keep service boundaries aligned with business capabilities.
Most teams don’t set out to build a distributed monolith. They start with a reasonable goal—independent deployability, team autonomy, and scaling services separately—and end up with all the operational overhead of distributed systems and none of the independence.
The problem worth solving
The tell is almost always the same. A “simple” feature change requires coordinated deployments across four or five services. A single user request fans out into a dozen synchronous calls. Nobody can deploy orders-service without checking whether inventory-service and pricing-service are compatible.
That’s not microservices. That’s a monolith chopped along the wrong lines and shipped over HTTP.
Draw boundaries around business capabilities, not technical layers
The highest-leverage decision in a microservices architecture is where you cut. Teams that split along technical layers—a “database service,” a “validation service,” or a “notification service”—create services with no independent business reason to exist. Every meaningful operation touches several of them, so coupling moves from code into the network.
Domain-Driven Design’s bounded context remains a useful tool. Ask what part of the business has its own vocabulary, rules, and reason to change independently. Billing changes for different reasons than Fulfillment. Catalog evolves on a different rhythm than Search. Those are credible boundaries.
If you can’t describe a service’s responsibility in one sentence without using the word “and,” the boundary is probably wrong.
Own your data, not just your code
A service isn’t independent if other services reach directly into its database. Shared databases are a common cause of accidental coupling: services look separate in the architecture diagram, but one schema change breaks several consumers.
Each service should own its data exclusively. Other services receive access through its API or events—not through a shared table. A shared read replica may look like the fastest path today, but it often becomes the reason nobody can change anything six months later.
Prefer asynchronous communication where the business allows it
Synchronous chains are where distributed monoliths are born. If Checkout calls Inventory, which calls Pricing, which calls Tax, you have created one failure domain with network latency added.
Where the process tolerates it, publish an event and let downstream services react independently. One unavailable consumer should not take the others down.
{ order.placed } → [ event bus ] → inventory → notifications → analytics
This is not a rule for every interaction. Some operations genuinely need an immediate answer. The engineering judgment is knowing which interactions can be decoupled in time.
Design the contract, not just the code
A service’s REST endpoints and event schemas are contracts with every consumer. Version them deliberately. Add fields in backward-compatible ways. Treat a breaking contract change with the same seriousness as a breaking public API.
Key takeaways
- Name bounded contexts in the business domain before naming services.
- Give every service exclusive ownership of its data.
- Use asynchronous communication when an immediate response is unnecessary.
- Treat synchronous dependencies as deliberate exceptions.
- Version APIs and events as long-lived contracts.
Independent deployability is the outcome of good boundaries. It cannot be bolted on afterward with better CI/CD.
