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

How to Design APIs That Survive Version Changes

APIs don’t break all at once. They slowly drift until something snaps. Good design isn’t about avoiding change — it’s about surviving it.

Read more

Burnout in Software Engineering Looks Different Than You Expect

Engineering burnout rarely announces itself as exhaustion. It shows up as detachment, cynicism about work that used to matter, and a grinding decline in the quality of your judgment — often months before you recognize what's happening.

Read more

Why Your API Returns 200 Even When Something Goes Wrong

Returning HTTP 200 for failed operations hides errors, breaks client logic, and makes systems harder to debug. Using proper status codes is not pedantry—it’s critical for correctness and reliability.

Read more

From Figma to API: A Structured Backend Development Process

You got a gorgeous Figma design and think, “Easy, backend can just follow this, right?” Not so fast. Without a clear technical plan, even perfect screens can lead to messy APIs.

Read more