⏱ 20 min read
Microservices design usually begins with the wrong question.
Teams ask, “How small should a service be?” That sounds practical, almost engineering-minded. But it is the architectural equivalent of asking how thin to slice bread before deciding whether you are making toast, sandwiches, or breadcrumbs. Size is not the first concern. Semantics are. Business capability is. Change is. Ownership is. The shape of failure is. Only then does size matter.
This is why service granularity remains one of the most abused ideas in modern enterprise architecture. Some teams build distributed monoliths by carving a monolith into dozens of tiny services with shared databases and synchronized deployments. Others swing the other way and create oversized “platform” services that become mini-monoliths with APIs. Both are expensive mistakes. The first dies from chatty coupling. The second dies from internal entropy.
Granularity is not a number. It is a set of heuristics applied to a living system.
That distinction matters. In real enterprises, services are not born into clean greenfield conditions. They arrive in a world of legacy ERPs, Kafka topics nobody wants to own, quarterly funding gates, compliance controls, duplicated customer records, and teams with uneven skills. The architecture that survives is the one that can explain not only the ideal target state, but also the migration path, the reconciliation strategy, and the operational burden.
So let’s be blunt: there is no universal “right” service size. There is only a granularity spectrum, and your job is to decide where each capability belongs on it. Good architects do that by reading the domain, not by following slogans.
Context
Microservices emerged as a response to a very real enterprise problem: large systems became too hard to change. Releases were risky. Teams stepped on each other’s code. One overloaded module could drag down an entire application. The monolith, especially the unmanaged one, often turned into a traffic jam disguised as a codebase.
Microservices promised a different path. Independent deployability. Team autonomy. Better fault isolation. Polyglot freedom where justified. Faster alignment between software and business capability. In many cases, they delivered.
But the promise came with a tax. Every boundary becomes an operational and cognitive cost. A method call becomes a network call. An in-process transaction becomes a saga or compensation flow. Debugging moves from stack traces to log correlation, distributed tracing, and event reconstruction. Data consistency becomes a design choice rather than a default. The architecture no longer hides distributed systems problems. It puts them on your desk.
That is why granularity matters so much. The number and shape of service boundaries determine whether microservices amplify business agility or merely spread complexity into smaller boxes. microservices architecture diagrams
Domain-driven design gives us the best language for thinking about this. Services should not be carved around tables, endpoints, or organizational folklore. They should emerge from business meaning: bounded contexts, aggregates, invariants, domain events, and the seams where language and responsibility genuinely differ. Granularity is, at heart, a domain question with technical consequences.
Problem
The central problem is deceptively simple: how do we decide whether a capability belongs inside an existing service, inside a new service, or inside a larger domain platform?
In practice, this question surfaces in many forms:
- Should pricing be part of the order service, or its own service?
- Should customer profile, consent, preferences, and identity live together?
- Should inventory reservation and stock visibility be one service or several?
- Should fraud decisioning be synchronous or event-driven?
- Should a reporting use case get its own read model service?
- Should a legacy fulfillment module be split now, or wrapped first and strangled later?
These are not merely decomposition choices. They affect team boundaries, runtime coupling, data ownership, resiliency, regulatory controls, and migration cost.
Too coarse, and a service accumulates unrelated responsibilities. Change becomes slower. Teams collide. Internal modules become tightly coupled. Scaling characteristics diverge and are forced into one deployment unit.
Too fine, and you pay the “nano-service” tax. Latency rises. Failure surfaces multiply. Cross-service workflows become orchestration nightmares. Data consistency gets harder. Teams spend more time integrating than delivering.
The trick is not to find the perfect granularity. The trick is to find a granularity that is coherent for the domain, economical to operate, and realistic to migrate toward.
Forces
Several forces pull granularity decisions in different directions. Good architecture lives in these tensions.
1. Domain cohesion
The strongest force is semantic cohesion. Things that change for the same business reason often belong together. If two capabilities participate in the same invariants, share the same ubiquitous language, and are understood by the same domain experts, splitting them too early usually causes harm.
For example, order placement and order line validation may belong together if they are part of one transactional consistency boundary. But product catalog browsing and order capture usually do not, even if they appear in the same UI.
This is where bounded contexts matter. A bounded context is not just a module boundary. It is a linguistic and conceptual boundary. If “customer” means account holder in one area and consumer identity in another, forcing them into one service because they share a noun is architectural laziness.
2. Transactional consistency
Some business rules demand immediate consistency. Others tolerate eventual consistency. This distinction often decides whether a boundary is practical.
If a set of operations must be atomic to maintain domain invariants, they are strong candidates to remain within one service and one transactional boundary. If they can be coordinated through events, retries, reconciliation, and compensations, then decomposition becomes more viable.
A lot of over-decomposition happens because architects underestimate the cost of replacing ACID transactions with asynchronous coordination.
3. Rate of change
Capabilities that change at different speeds are often better separated. Pricing algorithms may evolve weekly, while product master data may change more slowly. Tax calculation may change on regulatory cycles. Splitting by change cadence can reduce deployment friction.
But this is not absolute. If two capabilities change together because the business process itself changes together, then separation merely adds release choreography.
4. Team cognitive load
This is the practical force people ignore until production hurts. A service should be understandable by the team that owns it. If a service becomes too broad for a team to reason about, decomposition is justified. If the service is so tiny that no team can own meaningful business outcomes without coordinating with five others, decomposition has gone too far.
Conway’s Law is not a theory here. It is gravity.
5. Runtime coupling and communication style
Synchronous HTTP boundaries are more expensive than in-process calls. Event-driven boundaries through Kafka reduce temporal coupling, but they introduce asynchronous complexity, idempotency concerns, ordering questions, replay implications, and schema evolution problems. event-driven architecture patterns
A service split that looks elegant on a whiteboard may turn ugly if every business interaction requires six synchronous calls in the request path.
6. Data ownership and integration reality
In enterprise settings, data rarely starts clean. There are existing systems of record, multiple masters, inconsistent identifiers, and hard regulatory rules around retention and audit. Service granularity has to respect where truth actually resides today, not where architecture slides wish it lived.
This is especially important during migration. A service is not “autonomous” if it can only function through direct reads from a shared legacy database.
Solution
The solution is not a formula. It is a set of heuristics applied deliberately. My view is simple: start with business capabilities and bounded contexts, then adjust granularity based on consistency needs, change patterns, and operational economics.
Think in terms of a spectrum.
The “right-sized” service in the middle is usually the target. Not because moderation is morally superior, but because that is where domain cohesion and practical operability most often intersect.
Here are the heuristics I would use.
Heuristic 1: Split on business meaning, not technical layers
Do not create separate services for controller, workflow, rules, and repository concerns. That is layer decomposition masquerading as architecture. A service should encapsulate a business capability end to end.
“Order Service,” “Inventory Service,” and “Billing Service” are plausible. “Validation Service,” “Database Service,” and “Rules Service” usually are not unless they represent true domain capabilities with independent ownership and semantics.
Heuristic 2: Keep invariants inside the boundary
If a domain invariant must be enforced immediately, keep the behavior and data needed to enforce it within one service when possible.
For example, if “an order cannot be confirmed unless all lines are reserved or backordered according to policy” is a hard invariant, then the transaction boundary matters. Splitting reservation logic into a separate synchronous service may be possible, but it should be done with full awareness of the consistency and failure consequences.
Heuristic 3: Use events to cross context boundaries, not to avoid thinking
Kafka is powerful when it carries domain events across bounded contexts: OrderPlaced, PaymentAuthorized, InventoryReserved, ShipmentDispatched. It is less useful when it becomes a dumping ground for internal CRUD mutations that leak one service’s data model into the rest of the estate.
Event-driven architecture should reinforce domain boundaries, not dissolve them.
Heuristic 4: Favor service autonomy over superficial reuse
A common anti-pattern is extracting a tiny shared capability into a central service because multiple teams need it. This often creates a dependency magnet. Every team queues behind it. The service becomes a governance bottleneck. EA governance checklist
Reuse is not free. In distributed systems, excessive reuse often centralizes coordination and slows the enterprise. Sometimes duplication across bounded contexts is cheaper and healthier than a “shared enterprise service.”
Heuristic 5: Defer splitting until there is a real force
Greenfield teams often over-split because they fear future coupling. The result is speculative architecture. If there is no clear domain seam, no independent scaling need, no distinct team ownership, and no different change cadence, keep the capability together.
A service boundary should earn its existence.
Heuristic 6: Design for reconciliation from the beginning
Once services own separate data and communicate asynchronously, discrepancies are not edge cases. They are normal operating conditions. Every serious microservices architecture needs reconciliation processes: replay, compensation, correction workflows, dead-letter handling, audit views, and operator tooling.
If your decomposition creates eventual consistency, your architecture must include the machinery to detect and repair divergence.
Architecture
A useful microservices architecture for sane granularity typically combines synchronous APIs within immediate business interactions and Kafka-based domain events for cross-context propagation.
The shape often looks like this:
There are a few important ideas buried in this diagram.
First, not every interaction should be asynchronous. Order placement may need an immediate answer. Payment authorization may be synchronous in the request path. Inventory reservation might be synchronous or asynchronous depending on the business model. You choose based on user expectation, failure tolerance, and consistency requirements.
Second, Kafka is not the center of the universe. It is an integration backbone, not a substitute for service design. If every service depends on every topic, you have simply built a pub-sub monolith.
Third, read models and analytics often deserve separate treatment. They are excellent candidates for event-driven projections because they tolerate eventual consistency and can be rebuilt from event history if the event contracts are designed well.
Domain semantics and bounded contexts
The architecture should reflect domain semantics. In retail, for example:
- Catalog manages product presentation and search attributes.
- Pricing manages price calculation, promotion rules, and effective dates.
- Order Management owns order lifecycle and customer commitment.
- Inventory owns stock position and reservation logic.
- Fulfillment owns picking, packing, and shipment execution.
- Customer Identity owns authentication and profile identity.
- Customer Engagement may own preferences, loyalty, and communication consent.
Notice that “customer” is not one thing. Identity, profile, consent, loyalty, and account hierarchy may belong to different bounded contexts. If you cram them into one Customer Service, you have not simplified the domain. You have blurred it.
Internal modularity still matters
One of the dirty secrets of microservices is that many systems would be healthier if teams first built a well-modularized monolith. Internal modules with clear boundaries and tests are often the proving ground for future service extraction. The service boundary should follow demonstrated modular seams, not wishful thinking.
A bad monolith split into microservices does not become good architecture. It becomes remote bad architecture.
Migration Strategy
This is where theory meets enterprise weather.
Most organizations do not get to redraw the system from scratch. They have a large existing platform, a shared relational schema, nightly batch jobs, and channels that cannot be disrupted. So granularity decisions must be made with migration in mind, not just target-state purity.
The right pattern here is progressive strangler migration. Wrap, route, extract, reconcile, then retire.
Step 1: Identify stable seams
Do not start with the hardest, most entangled capability unless there is no alternative. Start where the domain seam is visible and the business value is clear. Customer notification, pricing calculation, product content, or returns initiation are often more tractable than core order orchestration.
Look for capabilities with:
- distinct business ownership
- manageable data scope
- low transactional entanglement
- strong need for change
- measurable pain in the current system
Step 2: Introduce a facade or routing layer
A routing layer lets channels talk to a stable interface while traffic is gradually shifted from legacy functions to new services. This matters because migration rarely happens in one release. It is a sequence of selective substitutions.
Step 3: Establish data movement and event publication
If the legacy platform still owns source data, you need a disciplined integration pattern. That might involve change data capture, outbox patterns, or explicit domain event publication. Avoid direct database reads by new services if you can. They create fake autonomy and freeze the legacy schema in place.
Step 4: Run parallel and reconcile
This is the part many teams skip because it feels operational rather than architectural. It is architectural.
When a new service starts owning behavior or state previously managed by the monolith, divergence is inevitable. Messages arrive late. Legacy corrections happen outside the new flow. Duplicate events occur. Human operators override statuses.
So you need reconciliation:
- compare legacy and new state
- flag mismatches
- define authority rules
- support replay and reprocessing
- provide human review where automatic repair is unsafe
Reconciliation is the bridge between idealized eventual consistency and messy enterprise reality.
Step 5: Move ownership, not just traffic
A strangler migration is complete only when the new service owns its behavior, data, and operational accountability. If every incident still requires digging into the legacy system, then the new boundary is cosmetic.
Enterprise Example
Consider a global retailer modernizing its order-to-fulfillment estate.
The company had a fifteen-year-old commerce monolith supporting web, mobile, and in-store assisted ordering. Everything touched the same database. Product data, pricing, customer profile, orders, stock, shipment status, promotions, and returns all lived in one sprawling schema. Release windows were monthly. Peak-season changes were frozen for weeks.
The initial instinct from leadership was predictable: “Break the monolith into microservices.” The first vendor proposal suggested more than forty services, including promotion service, tax service, discount service, cart validation service, customer preference service, shipment event service, and loyalty points service. On paper it looked modern. In practice it would have created a support nightmare.
The architecture team instead used domain-driven design workshops with product, fulfillment, finance, and store operations. They mapped event storming sessions across order lifecycle scenarios: browse, reserve, purchase, split shipment, cancellation, return, refund, substitution, and store pickup.
What emerged was revealing. Pricing and promotions were tightly coupled semantically but had a different change cadence from catalog. Inventory availability and reservation needed distinct treatment from warehouse execution. Customer identity and marketing preferences were conflated in legacy but were owned by different business groups with different compliance needs. Returns was not merely an extension of orders; it had its own workflow, policies, and fraud concerns.
The resulting first-wave service design was deliberately restrained:
- Catalog
- Pricing
- Customer Identity
- Consent & Preferences
- Order Management
- Inventory
- Fulfillment
- Returns
Not tiny. Not massive. Right-sized enough to support distinct ownership and evolution.
Order Management remained a relatively broad service because its invariants were strong and the migration risk was high. The team resisted pressure to split order orchestration, order lines, order status, and cancellation into separate services. That would have made the most critical business flow depend on distributed coordination before the organization was ready.
Kafka was introduced as the event backbone for cross-context communication. OrderPlaced triggered downstream fulfillment preparation and analytics. InventoryAdjusted updated availability views. ShipmentDispatched fed customer notifications and tracking views. But payment authorization remained synchronous because the customer could not wait on eventual consistency at checkout.
Migration followed a strangler pattern. A facade fronted the monolith APIs. Catalog and pricing were extracted first because they had clear seams and high business change pressure. Order Management was partially wrapped before being progressively reimplemented for selected order types. During the transition, legacy stock adjustments and new inventory reservations could disagree, especially during store operations and manual corrections. Reconciliation jobs compared expected and actual reserved quantities, raising operator tasks for anomalies.
This was not glamorous architecture. It was enterprise architecture in the only form that matters: architecture that survives contact with operations.
The result after eighteen months was not “full microservices.” It was better. Release frequency increased dramatically in the extracted domains. Checkout incidents decreased because the request path was simplified rather than atomized. Teams gained autonomy where the domain justified it. And crucially, the business could keep selling while the migration happened.
Operational Considerations
Granularity is paid for in operations.
Every new service introduces pipelines, observability, alerting, dashboards, secrets, runtime patching, capacity planning, dependency management, and on-call responsibility. Architects who ignore this are merely drawing invoices for the platform team.
A few operational concerns should directly influence service granularity.
Observability
Fine-grained services need distributed tracing, structured logs, correlation IDs, and event lineage. Without them, incident response becomes archaeology. A simple user failure may span an API gateway, order service, payment provider, Kafka topic, fulfillment consumer, and notification service.
Schema evolution
With Kafka in the mix, event contracts become products. You need schema versioning, compatibility rules, consumer testing, replay strategy, and topic ownership discipline. If producers casually change event shape, granularity turns into ecosystem fragility.
Idempotency
At-least-once delivery means duplicate processing will happen. If your service boundary depends on event-driven coordination, idempotent handlers are not optional. Neither are deduplication keys and safe retries.
Backpressure and retry storms
Small services fail in packs. A slowdown in one dependency can trigger retries across many callers, amplifying load and causing cascading failure. Granularity decisions should account for this. A split that creates heavy synchronous fan-out in a hot path is a reliability risk.
Data retention and audit
In regulated enterprises, services often need auditability, lineage, and retention controls. Event streams help, but they also create obligations around PII, redaction, and access control. Do not split sensitive data domains casually.
Tradeoffs
There is no free lunch here. A service boundary is always a trade.
Coarse-grained services give you simpler runtime flows, easier transaction management, and lower operational overhead. But they can become change bottlenecks and blur responsibility.
Fine-grained services give you sharper ownership, more selective scaling, and cleaner domain separation when the domain truly supports it. But they introduce network latency, data duplication, eventual consistency, and governance complexity. ArchiMate for governance
A useful rule of thumb is this: if the cost of coordination exceeds the value of independence, the boundary is too fine.
Another: if a team cannot explain in one sentence what business capability a service owns, the boundary is probably wrong.
And another: if every user request fans out across half the architecture, the design is not modular. It is fragmented.
Failure Modes
Granularity mistakes tend to fail in recognizable ways.
Distributed monolith
The classic. Many services, tightly coupled releases, synchronous chains everywhere, shared database hiding underneath. It has all the complexity of microservices and none of the autonomy.
Shared data backdoor
Teams claim service ownership but continue reading and updating the same legacy tables directly. The database remains the true integration layer. API boundaries become theater.
Nano-services
Capabilities are split into pieces too small to own business outcomes. Teams spend their time negotiating contracts and debugging interactions rather than improving the domain.
Event soup
Kafka topics proliferate without semantic discipline. Events are CRUD notifications, not domain events. Consumers become dependent on internal implementation details. Replay breaks assumptions. Nobody knows which topic is authoritative.
Saga sprawl
Too many cross-service workflows require orchestration or compensation. Business logic leaks into integration layers. Failure handling becomes unpredictable. Testing turns miserable.
Reconciliation blindness
Architects design eventual consistency but omit reconciliation processes. Discrepancies accumulate silently until finance, operations, or customers discover them first.
When Not To Use
Microservices with deliberate service granularity are not always the right answer.
Do not use this style if:
- the domain is small and stable
- a single team can comfortably own the system
- operational maturity is weak
- deployment frequency does not justify the complexity
- transactional consistency is dominant across most of the domain
- the organization lacks observability, platform engineering, and disciplined API/event governance
In these cases, a modular monolith is often the better architecture. It gives you domain boundaries, testable modules, and lower operational burden. You can still apply domain-driven design. In fact, you should. A modular monolith is not a compromise. Often it is the grown-up choice.
Related Patterns
Several related patterns often travel with granularity decisions:
- Bounded Context for semantic separation
- Strangler Fig for incremental migration
- Outbox Pattern for reliable event publication
- Saga for long-running distributed business processes
- CQRS for separating operational writes from read-optimized projections
- Backend for Frontend to prevent channels from coupling to internal service topology
- Anti-Corruption Layer when integrating with legacy or packaged systems
- Modular Monolith as a precursor or alternative to microservices
The point is not to collect patterns like badges. The point is to combine them in service of domain clarity and migration safety.
Summary
Service granularity is not a purity contest. It is a design judgment made under business pressure, technical constraint, and organizational reality.
The best heuristic is still the oldest one: follow the domain. Use bounded contexts to find semantic seams. Keep strong invariants inside coherent boundaries. Split when change cadence, ownership, or scaling genuinely justify it. Resist speculative decomposition. Use Kafka to propagate meaningful domain events, not to paper over bad boundaries. Build reconciliation into the design, especially during migration. And migrate progressively, with a strangler approach that respects the fact that enterprises must keep running while architecture evolves.
If you remember one line, make it this: a service boundary should reduce the cost of change more than it increases the cost of coordination.
That is the whole game. Everything else is diagram styling.
Frequently Asked Questions
What is a service mesh?
A service mesh is an infrastructure layer managing service-to-service communication. It provides mutual TLS, load balancing, circuit breaking, retries, and observability without each service implementing these capabilities. Istio and Linkerd are common implementations.
How do you document microservices architecture for governance?
Use ArchiMate Application Cooperation diagrams for the service landscape, UML Component diagrams for internal structure, UML Sequence diagrams for key flows, and UML Deployment diagrams for Kubernetes topology. All views can coexist in Sparx EA with full traceability.
What is the difference between choreography and orchestration in microservices?
Choreography has services react to events independently — no central coordinator. Orchestration uses a central workflow engine that calls services in sequence. Choreography scales better but is harder to debug; orchestration is easier to reason about but creates a central coupling point.