How We Decide Whether a Microservice Should Actually Be a Microservice
Use a microservice when running something as a separate service makes a specific problem meaningfully easier: a clear domain boundary, a different scaling profile, independent releases, failure isolation, or a security or compliance boundary. When none of those apply, a modular monolith gives you the same clarity with far less to operate.
Written for CTOs, founders, and engineering leaders deciding how to structure a growing system, or untangling one that was split too early.
The short answer
- Start with one question: what becomes meaningfully easier if this is a separate service?
- Separate when a strong domain boundary meets a real need: independent scaling, releases, ownership, failure isolation or compliance.
- Keep it together when one team owns it, it shares data and a release cycle, and splitting adds no business value.
- Every service adds pipelines, infrastructure, observability, contracts and on-call. Budget for years, not the first sprint.
- Services that must change and deploy together are a distributed monolith, not microservices.
- A modular monolith keeps the boundaries and the option to extract a service later.
What problem becomes meaningfully easier if this is a separate service?
That is the first question we ask, and it needs a specific answer. “It’s more scalable” or “it’s more modern” is not one. “Billing has to ship on its own schedule because finance signs off on every change” is.
A microservice should exist because it solves a real problem, not because microservice architecture sounds more sophisticated. These are the problems a service boundary can genuinely solve:
- Clear domain boundaryThe capability has its own rules, language and data, and the rest of the system only needs a narrow interface to it.
- Independent deploymentIt needs to ship on its own schedule, and today it waits on, or blocks, unrelated releases.
- Independent scalingIts load looks nothing like the rest of the system, such as a bursty import job next to steady page traffic.
- Team ownershipA separate team owns it end to end and is regularly slowed down by coordinating changes in a shared codebase.
- Failure isolationWhen it fails, the rest of the product has to keep working, and a process boundary is the most reliable way to guarantee that.
- Security or compliance boundaryIt handles data, such as card or health data, where a smaller audited surface reduces real risk and cost.
- Data ownershipIt can own its data outright, so other parts of the system ask it for data instead of reading its tables.
- Different performance requirementsIt needs a different runtime, language, memory profile or latency budget from everything around it.
- Different release cyclesIt changes at a very different pace, such as a stable ledger next to a fast-moving customer-facing interface.
One reason on this list is rarely enough on its own. The strongest cases combine a clear boundary with at least one operational need the current structure can’t meet. And the same need often has a cheaper answer first: a separate worker pool for scaling, a read replica for reporting load, or an enforced module boundary for team ownership.
What does a microservice actually cost?
Creating a microservice is easy. Owning one for the next five years is the expensive part. The first deploy takes a day. The pipeline, the dashboards, the contract and the pager stay with you for as long as the service exists.
- Build and releaseCI/CD pipelineTestingDatabase migrationsIts own pipeline, test suite and migration history.
- RuntimeInfrastructureNetworkingAuthentication and authorizationSecrets
- VisibilityLoggingMonitoring and alertingDistributed tracing
- CommunicationRetries and failure handlingAPI contracts and versioning
The less visible costs are the ones that change how a team works. A function call becomes a network call that can be slow, fail, or partly succeed. A refactor that touched two files becomes a coordinated change across two repositories and a versioned contract. A bug that used to sit in one stack trace is now spread across two sets of logs, and finding it depends on tracing someone remembered to set up.
None of this is a reason never to build a service. It is the price, and the benefit should clearly exceed it. Martin Fowler calls this the microservice premium. It also shows up on the cloud bill: every service brings its own compute, load balancing and log volume, which is one of the places we look first when reducing AWS costs.
What is a distributed monolith?
A distributed monolith is a system split into services that still have to change, deploy and fail together. It keeps all the coupling of a monolith and adds the network, the extra infrastructure and the harder debugging of microservices. It is one of the worst outcomes in software architecture, because it costs the most and returns the least.
- Looks like
- Three independent services
- Behaves like
- One application that must change, deploy and fail together, with network calls in the middle
Service A calls Service B, which calls Service C, and all three read and write the same database. If a pricing change needs edits in all three and a coordinated release, we haven’t created three independent systems. We’ve created one system with network calls in the middle, and every one of those calls is a new way for a request to fail.
It usually happens in one of three ways: the system was split by technical layer or by database table instead of by business capability; it was split before anyone understood where the real boundaries were; or the shared database was kept “just for now” and never separated.
Signs you have a distributed monolith
- A typical feature needs coordinated changes in several services.
- Services have to be deployed together, or in a specific order.
- Services read and write each other’s tables in a shared database.
- One slow or failed service takes the whole request path down with it.
- Testing one change locally means running most of the system.
- Changing an API contract needs a meeting with every team that calls it.
The fix is not always more separation. Sometimes it is moving data ownership so each service owns its tables. Often it is merging services that always change together back into one well-structured application. Consolidating an over-split system is legitimate application modernization, and it frequently makes a team faster and the system cheaper to run.
What is a modular monolith, and why do we often recommend it?
A modular monolith is one deployable application divided internally into modules with clear boundaries. Each module owns its own logic and data and exposes a narrow interface to the others. You keep most of the clarity of services without running a distributed system.
| Concern | Modular monolith | Microservices |
|---|---|---|
| Domain boundaries | Enforced in code: module interfaces, import rules, table ownership | Enforced by the network and separate codebases |
| Deployment | One pipeline, one release | A pipeline and a release per service |
| Local development | Run one application and one database | Run several services, or rely on stubs and shared environments |
| Transactions | Ordinary database transactions across modules | Sagas, outbox patterns and eventual consistency |
| Infrastructure cost | One set of compute, database and monitoring | Repeated per service, plus networking and gateways |
| Operational overhead | One thing to monitor, secure and upgrade | Many things, with tracing to connect them |
| Independent scaling | Scale the whole application, or run one module’s workers separately | Scale each service on its own |
| Team autonomy | Good with clear module ownership, on a shared release | Highest, if services are truly independent |
A module with a clean interface and its own tables is also the cheapest possible starting point for extracting a service later.
A modular monolith gives a growing product clear domain boundaries, simpler deployment, easier local development, ordinary transactions, lower infrastructure cost and less operational overhead, while preserving boundaries that can become services if the business actually needs them.
Choosing the simpler architecture is not unsophisticated engineering. Adding complexity without receiving meaningful value from it is.
The discipline is what makes it work. Boundaries that exist only on a diagram erode within months. We enforce them in the code: group code by business capability rather than technical type (in Django, that means apps with real boundaries, as described on our Python and Django development page), give each module ownership of its tables, route cross-module calls through a small public interface, and add import rules to the build so violations fail the pipeline. Tools such as import-linter for Python and Packwerk for Rails exist for exactly this. Shopify has written publicly about structuring its large Rails monolith into components instead of splitting it into services.
Microservices as an evolution, not a day-one requirement
We treat a microservice as something a system can grow into when the evidence appears. The boundary comes first. The network comes last.
- Monolith
One codebase, one deploy. The fastest way to learn what the product actually is.
- Modular monolith
Code grouped by business capability, each module with its own data and interface.
- Clear domain boundary
One module’s boundary holds steady: few cross-module changes, a narrow interface.
- Independent scaling or ownership needEvidence
A real need the monolith can’t meet cheaply: scaling, release cadence, ownership, isolation or compliance.
- Microservice
Extract that module behind the interface it already has. Everything else stays put.
Most of a system’s code never needs to move past the second or third step, and that is a good outcome. When a service does make sense, extracting a module that already has a clean interface and its own tables is mostly mechanical. Extracting logic tangled through a codebase is a project in itself, which is why the boundary work pays off whether or not a service ever follows. Martin Fowler describes the same path as Monolith First.
For an existing system, the Strangler Fig pattern lets traffic move to the new service gradually while the original keeps running, so extraction never requires a risky cutover. It is also one of the safer alternatives to a full rewrite, which we cover in should you rewrite your application from scratch?
When should a service have its own database?
When it is genuinely a separate service. A service that shares tables with other services isn’t independent, because any schema change becomes a coordinated release.
Owning its data is what lets a service change on its own. Other parts of the system get that data through the service’s API or through events it publishes, never by reading its tables directly. Reporting and analytics usually read from a replica or a warehouse that is fed from those sources. This is the database-per-service pattern.
“Its own database” doesn’t have to mean its own database server. A separate schema with its own credentials enforces ownership at a fraction of the cost, and it is often the right first step.
If two services can’t do their job without joining each other’s tables, treat that as evidence about the boundary, not a reason to share the database. They are probably one domain, and should be one service or one module.
Where Kafka and event-driven architecture fit
Kafka is a tool for specific problems, not an upgrade over REST. Asynchronous messaging removes some kinds of coupling and introduces a different kind of complexity.
Event-driven communication earns its place when it delivers real benefits:
- Decoupling. An orders service publishes “order placed” without knowing that billing, email and analytics react to it. Adding a new consumer doesn’t change the producer.
- Throughput and buffering. A durable log absorbs spikes so downstream systems process work at their own pace instead of falling over.
- Independent consumers. Several systems read the same stream for different purposes, each tracking its own position.
- Event processing and replay. Kafka keeps messages for a configurable retention period, so a new consumer can read history, and stream processing can aggregate or transform events as they arrive.
It also brings real operational complexity: a broker cluster to run, or a managed service such as Amazon MSK or Confluent Cloud to pay for; schemas that have to evolve without breaking consumers; ordering that is guaranteed only within a partition; delivery that is typically at least once, so every consumer must be idempotent; dead-letter handling; consumer lag to monitor; and eventual consistency that users can see. Answering “what happened to this order?” means following an event across topics and consumers rather than reading one trace.
| Question | Synchronous API (REST, gRPC) | Events (Kafka or a queue) |
|---|---|---|
| Does the caller need an answer now? | Yes: the response drives the next step | No: the work can happen after the request returns |
| How many consumers? | One known caller and one known provider | Several, and more will be added |
| Consistency | Immediate and easy to reason about | Eventual; the interface has to handle “processing” |
| Failure mode | The caller sees the error and can retry | The message waits or retries; consumers must be idempotent |
| Debugging | Follow one request through a trace | Follow an event across topics, consumers and lag |
| Operational load | The load balancer and services you already run | A broker or managed service, schema management, lag monitoring |
Most healthy systems use both: synchronous calls for queries and commands that need an answer, events for facts that other parts of the system react to.
Not every asynchronous need calls for Kafka. A job queue such as Celery with Redis, Amazon SQS, or a transactional outbox table in PostgreSQL covers a great deal of background work with far less to operate. We reach for Kafka when there is real event volume, several independent consumers, or a need to replay history.
A practical decision framework
The microservices vs monolith question is rarely about the whole system. It is about one capability at a time. Default to keeping things together, and separate when the evidence is specific.
Consider separating it when
- There is a strong, stable domain boundary
- A different team owns it end to end
- Its scaling characteristics differ sharply from the rest
- It must deploy independently, and today that is blocked
- Its failure must not take down the rest of the product
- It sits behind a security or compliance boundary
Keep it together when
- The same team owns the functionality
- It shares the same lifecycle and release cadence
- It reads and writes the same data
- Scaling requirements are similar
- Separating it creates no meaningful business or operational value
Choose where each factor points for your situation.
- Domain boundary
Separate: Its own rules, language and data, with a narrow interfaceKeep together: Shares concepts and tables with its neighbors
- Ownership
Separate: A different team owns it end to endKeep together: The same team owns the code around it
- Scaling
Separate: Its load profile is very different from the restKeep together: It scales with everything else
- Release cadence
Separate: It must ship independently, and is blocked todayKeep together: It ships fine with the rest of the application
- Failure isolation
Separate: Its outage must not affect core flowsKeep together: Its failure would hurt the same flows anyway
- Security or compliance
Separate: Separating it shrinks audit scope or limits access to sensitive dataKeep together: Same data classification as the rest
- Operational capacity
Separate: The team already runs services with CI/CD, tracing and on-callKeep together: Another service would stretch what the team can operate
A thinking aid, not a formula. A single factor, such as a hard compliance requirement, can outweigh all the others.
Three examples of the decision
Illustrative scenarios built from common patterns, not records of client engagements.
Orders, invoicing and customer accounts in a B2B portal: keep together
One team builds all three. Invoices reference orders, orders reference accounts, and they ship on the same cadence under similar load. Splitting them would turn simple joins into API calls and a single database transaction into a saga, with no business benefit in return. Decision: one application with three modules, each owning its tables and exposing a small interface.
Document processing that spikes at month end: separate the worker first
Generating and parsing documents is CPU-heavy and bursty, and it slows down web requests when it runs alongside them. The first step is not a new service. It is moving that work to a background queue with its own worker pool, deployed from the same codebase, which gives independent scaling without a new contract. Decision: separate the process, not the codebase. Promote it to a service only if it later needs a different runtime, such as a GPU or a machine learning stack, or a different team takes ownership.
Card payment handling: separate service, or no service at all
Under PCI DSS, systems that store, process or transmit cardholder data fall into the assessment’s scope. Isolating card handling behind a small service with its own data store and access controls keeps the rest of the application out of that scope. The interface is narrow (charge, refund, tokenize) and the boundary is stable. Decision: a separate service, unless a payment provider’s hosted fields and tokens keep card data out of your systems entirely, which is simpler still.
How Yippify approaches the decision
Our principle is to build the simplest architecture that solves today’s problem without preventing tomorrow’s growth. In practice, that means we start from the problem rather than the pattern, draw boundaries around business capabilities, enforce them in code, and extract a service only when a specific need shows up and the team can own what it creates.
We write the important choices down as decision records, with the options and tradeoffs explicit, so the reasoning survives after the meeting ends. You can see an example on our software architecture consulting page. The same thinking runs through our approach to every project: use sophisticated tools when they meet a real need, and not to look sophisticated.
Frequently asked questions
When should you use microservices?
Use a microservice when running a capability as a separate service makes a specific problem meaningfully easier: a strong domain boundary combined with a real need for independent scaling, independent deployment, separate team ownership, failure isolation, or a security or compliance boundary. If the only reason is that microservices seem more modern or scalable, keep it in the application.
When should you keep a monolith?
Keep functionality in one application when the same team owns it, it shares the same data and release cycle, its scaling needs are similar to the rest of the system, and separating it would not create meaningful business or operational value. For most growing products, a well-structured modular monolith is the right default.
What is a modular monolith?
A modular monolith is a single deployable application divided internally into modules with clear boundaries. Each module owns its own logic and data and exposes a narrow interface to the others. It offers clear domain boundaries, simple deployment, easy local development, ordinary transactions and lower infrastructure cost, while keeping the option to extract a module into a service later.
What is a distributed monolith?
A distributed monolith is a system split into services that still have to change, deploy and fail together, often because they call each other synchronously and share a database. It keeps the coupling of a monolith and adds network failures, extra infrastructure and harder debugging. Services that always deploy together are a strong sign of one.
Are microservices worth the complexity?
Sometimes. Every service adds a CI/CD pipeline, infrastructure, networking, authentication, secrets, logging, monitoring, tracing, API versioning, testing and on-call ownership for as long as it exists. Microservices are worth it when the benefit, such as independent scaling or team autonomy, clearly exceeds that ongoing cost. Creating a service is easy; owning it for years is the expensive part.
When should a service have its own database?
When it is genuinely a separate service. A service that shares tables with other services is not independent, because any schema change becomes a coordinated release. Other systems should get its data through its API or events. A separate schema with its own credentials can enforce ownership without a separate database server.
Is Kafka better than REST for microservices?
No. They solve different problems. Synchronous APIs suit requests that need an answer now and have one known caller. Kafka and other event-driven tools suit work that can happen later, several independent consumers, high event volume and replay. Kafka also adds a broker to run, schema management, idempotent consumers and eventual consistency, so many systems use both.
Can we move from a monolith to microservices later?
Yes, and that is usually the safer order. Start with a monolith, structure it into modules with clear boundaries and their own data, and extract a module into a service only when a specific need appears. The Strangler Fig pattern lets traffic move to the new service gradually while the original application keeps running.
Sources
Weighing a split, or living with one that went too far?
We help organizations with growing monoliths, microservice estates that cost more to run than they return, legacy application modernization, architecture decisions, cloud migrations, and custom software development where the architecture should be right-sized from the start.
Tell us the decision you’re facing and what happens if it goes wrong. We’ll give you a straight answer on what to separate, what to keep together, and why.
- A clear answer on what to separate, and what not to
- Decisions written down with tradeoffs
- Help implementing the plan with your team
A rough description is enough to start. No specification needed.