Lesson 59 of 129
Service Granularity
Service granularity is the size and scope of a service: how much functionality and data it owns. There is no correct size in lines of code. The right granularity is determined by how the system changes, what must be consistent together, and who owns it. Both extremes cause problems.
Too fine-grained (nanoservices)
A nanoservice is a service so small that its overhead exceeds the value of its independence.
Problems:
- Chatty communication. Completing one request needs many calls between services. Each call adds latency, a failure point, and serialization cost.
- Distributed transactions for simple operations. An invariant that spans two services can no longer use one local transaction.
- Operational overhead. Each service needs a pipeline, deployment, monitoring, alerting, and on-call.
- Deployment coupling. Services that always change together must still be released together.
- Version management. Many contracts between many services.
- Tail latency and availability. Long call chains lower availability (the product of the availabilities of each hop) and raise tail latency.
Too coarse-grained
A service that is too large keeps the problems of a monolith within its boundary:
- Many teams change the same service, so release coordination remains.
- A part with distinct scaling needs cannot be scaled separately.
- A failure or resource leak in one function affects all functions in it.
- Long build and test times.
- Ownership is unclear.
flowchart LR
subgraph fine["Too fine"]
A1["Address service"] --> A2["Customer service"]
A2 --> A3["Contact service"]
A3 --> A4["Preferences service"]
end
subgraph right["Cohesive"]
B1["Customer service: addresses, contacts, preferences"]
end
subgraph coarse["Too coarse"]
C1["Everything service: customers, orders, billing, catalog, shipping"]
endChatty services
A chatty interface requires many small requests from a client to complete one task. It is a symptom of a wrong boundary: data or behavior that changes together is on different sides of a network.
Example: rendering an order page requires 1 call for the order, then 1 call per line item to a separate pricing service. With 20 line items and 2 ms per call, the sequential calls add about 42 ms and 21 network failure points, compared with one in-process query when the data belongs to one service. If each call succeeds 99.9% of the time, the page succeeds about 0.999^21, roughly 97.9% of the time, before any retries.
Detection:
- Traces show many sequential calls between the same pair of services per request.
- One service's calls to another are almost always made together.
- A service is almost unusable without a specific other service.
Remedies:
| Remedy | When |
|---|---|
| Merge the services | They share an invariant or always change together |
| Coarser API (return what the caller needs in one call, batch endpoints) | The boundary is right but the API is too fine-grained |
| Replicate needed data through events into a local read model | The caller needs read access to another context's data (see eventual consistency and cross-service queries) |
| Move the behavior to the service that owns the data | Logic is in the wrong place |
| Parallelize independent calls or cache results | Calls are independent, or data changes rarely |
Criteria
Cohesion
A service should contain behavior and data that belong together: they serve one business capability, and are described with one consistent model (one bounded context, see domain-driven design). High cohesion within a service and low coupling between services is the goal.
Change frequency and reasons to change
Code that changes for the same reason should be in the same service. A single business change should ideally modify one service. Code that changes at different rates or for different reasons is a candidate for separation.
Data ownership and transaction boundaries
If two pieces of data must be updated atomically to preserve an invariant, they should be in the same service, because cross-service atomicity requires sagas and gives up isolation. Each service owns its data (database per service).
Team ownership
A service should be owned by one team, and a team can own several services. The size of a service should match what a team can understand, operate, and change. If understanding a service takes months or many teams need to change it, it is a candidate for splitting. If a team owns many tiny services and spends most of its time on their overhead, merging is a candidate (see Conway's law).
Scaling and availability needs
Components with very different load, resource profiles, or availability requirements benefit from separation. A component with a high-availability requirement should not share a process with one that crashes often.
Security and compliance
Separating components that handle sensitive data limits the scope of access control and audit.
Practical guidance
| Signal | Action |
|---|---|
| Services always deployed together | Merge |
| Feature changes touch 3 or more services routinely | Revisit boundaries |
| Service has multiple unrelated reasons to change | Split |
| Many teams edit one service | Split along contexts |
| One request involves long sequential synchronous chains | Merge, or replace calls with events or replicated data |
| Team cannot operate the number of services it owns | Merge |
Starting with somewhat coarser services and splitting when a real driver appears is easier than merging services that were split too early, because merging also requires combining data stores and reconciling contracts. A modular monolith (see monolith, modular monolith, SOA, and microservices) allows boundaries to be tested cheaply first.
Interview questions
- How do you decide the size of a service? By cohesion (one bounded context), reasons to change, data and transaction boundaries, team ownership, and scaling or compliance needs, not by lines of code.
- What does chatty communication between two services indicate? The boundary is probably wrong: data or behavior that changes together is split across a network. Merge the services, coarsen the API, or replicate the needed data.
- Why is it easier to split a service later than to merge services later? Splitting requires dividing code and data along a boundary that is now understood. Merging requires reconciling separate data stores, contracts, and consumers.
- When should two things be in the same service because of data? When they must change atomically to maintain an invariant, since cross-service atomicity requires sagas and eventual consistency.
