How Small Is a Microservice Supposed to Be
by Eric Hanson, Backend Developer at Clean Systems Consulting
The question that surfaces in every architecture review
Someone has drawn a service boundary, and the argument starts: is this too big? Should we split authentication and authorization into separate services? Should the user profile service be separate from user preferences? The debate goes in circles because "small" is not a well-defined property — people are using it to mean different things, and none of them are the right metric.
Size in lines of code is irrelevant. A 5,000-line service that owns a clean domain is better than a 500-line service that can't serve a request without calling three others. Size in database tables is irrelevant. A service that owns ten tables all within the same bounded context is better than one that owns two tables but has an implicit dependency on four others. Size in endpoints is irrelevant for the same reasons.
The debate about size is usually a proxy debate about something real: cohesion, coupling, and team ownership. Answer those questions directly.
The right measure: does the service own its domain?
A well-sized service can fulfill its primary use cases without synchronously depending on another service. This is the cohesion test. If your Order Service needs to call the User Service to check credit, call the Inventory Service to reserve stock, and call the Notification Service to confirm — all within a single request — then Order is not a cohesive service. It's a coordinator that owns no domain.
❌ Too fine-grained (every request requires orchestration)
User Request → Order Service
→ GET /users/{id}/credit (User Service)
→ POST /inventory/reserve (Inventory Service)
→ POST /notifications/send (Notification Service)
→ write order to DB
← return response
✅ Appropriately scoped (service owns its domain)
User Request → Order Service
→ check local credit snapshot (replicated via events)
→ write order + reservation to local DB (saga initiated)
→ publish OrderCreated event
← return response
→ Inventory Service (async, consumes OrderCreated)
→ Notification Service (async, consumes OrderCreated)
The second design is not possible unless Order Service has access to a credit snapshot or credit data has been modeled as part of the order domain. That data modeling decision is the real design work. Service sizing is a consequence of it.
The organizational alignment principle
A useful heuristic from Sam Newman's work on microservices: a service should be owned by a single team that can make autonomous decisions about it. If changing a service's behavior requires coordinating with another team, the service boundary is probably in the wrong place.
This gives you a concrete sizing rule: size services to match your team topology. If you have a payments team, you should have a payments service (or a small cluster of closely related payments services). The team should be able to deploy that service without a cross-team release coordination process. If they can't, either the service is leaking dependencies across team boundaries, or the team boundary is wrong.
The inverse is also true: if two teams are both modifying the same service, that's a signal the service contains multiple bounded contexts that should be separated — not necessarily into many services, but into clearly owned sub-domains.
When services are too small: the nanoservice problem
Teams that treat "small" as the goal end up with nanoservices — components so fine-grained they cannot function independently. Symptoms:
- A request touching six or more services synchronously to complete a single user action
- Services with one or two endpoints that delegate most logic to other services
- Shared utility services for things like "get the current timestamp" or "generate a UUID"
- Teams spending more time on service-to-service integration work than on domain logic
The operational cost of each additional service is non-trivial: a repository, a deployment pipeline, a container image, resource limits, health checks, monitoring dashboards, an on-call rotation entry. Multiplying that by thirty nanoservices for a product of modest complexity is an operational burden that benefits no one.
The minimum viable service size is: one bounded context, one team, one autonomous deployment. If a service is smaller than that, ask whether it should be a module inside a larger service rather than an independent deployment unit.
When services are too large: the distributed monolith problem
The opposite failure mode is a service that has grown to encompass multiple bounded contexts because splitting felt risky. A "Core Service" that handles users, orders, payments, and inventory is not a microservice. It's a monolith that happens to communicate with other services over HTTP.
The signal is again coupling: if the service's internal modules have to be deployed together, if changes to one concern regularly break others, if different parts of the service have significantly different scaling requirements, the service has grown past its cohesive boundary.
Splitting a too-large service is safer than splitting a monolith if the internal module boundaries are clean. Define the sub-domain boundaries inside the service first. Enforce them with module-level access controls. Extract when the boundary is stable and tested.
The practical answer
A microservice should be as large as its bounded context and as small as a single team can own autonomously. Lines of code, number of endpoints, and database table counts are not the variables. Cohesion and ownership are. When you're debating a service boundary, stop asking "is this too big?" and start asking "can this team deploy this independently?" and "does this service need to call others synchronously to do its job?" Those questions have answers.