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.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

The Problem With Always Reaching for the Latest Technology

New technology is appealing for legitimate reasons and problematic for systematic ones. The engineers who build the most reliable systems are the ones who evaluate novelty against operational reality, not against excitement.

Read more

Lessons From Failed Software Projects

Failure stings, but in software, it’s often a faster teacher than success. By analyzing what went wrong, teams can avoid repeating mistakes and build smarter, more resilient projects.

Read more

How to Laugh at Yourself After a Huge Mistake

We’ve all been there: the code breaks, the email goes to the wrong person, or the deployment wipes out production. Learning to laugh at these moments can save your sanity and even make you a better professional.

Read more

Window Functions: The SQL Feature That Changes How You Think About Data

Window functions let you compute aggregations across a set of related rows without collapsing them — once you understand the OVER clause, you stop writing self-joins and correlated subqueries to answer questions about relative position, running totals, and row-by-row comparisons.

Read more