How Small Is a Microservice Supposed to Be

by Arif Ikhsanudin, Backend Developer

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

Accidentally Publishing Half-Finished Code: How to Recover

You push your code, confident everything is ready… and then you realize part of it wasn’t supposed to go live.

Read more

Your API Gateway Should Be Doing More Than Just Routing

An API gateway that only proxies requests is an expensive reverse proxy. The gateway layer is the right place to enforce auth, rate limiting, request transformation, and observability — cross-cutting concerns that should not be reimplemented in every service.

Read more

How a Tech Lead Prevents Knowledge Silos and Technical Debt

Projects stall, bugs pile up, and only a few people understand critical systems. A strong tech lead ensures knowledge is shared and technical debt stays manageable.

Read more

The Pipeline Step Nobody Wants to Optimize Until It Hurts

Database migration steps in CI/CD pipelines are consistently the slowest, most brittle, and least examined part of the deployment process — until a failed migration takes down production.

Read more