The Question Behind the Question
"Should we use microservices?" is rarely the most useful question to ask, because the answer depends entirely on problems a team may or may not actually have yet. A more useful framing: what specific pain is the current architecture causing, and would splitting into services actually solve that pain, or just relocate it into new, different problems around network calls, distributed data consistency, and operational complexity that a monolith never had to deal with in the first place.
What a Monolith Actually Gets You
A single deployable application with a single codebase offers real, underrated advantages: a single database transaction can span everything an operation touches, refactoring across module boundaries is a normal code change rather than a coordinated multi-service rollout, and there is exactly one thing to deploy, monitor, and debug rather than a fleet of independently versioned services. For the large majority of applications, especially before significant scale, these advantages outweigh whatever theoretical benefits microservices promise.
What Actually Justifies Splitting a Service Out
Genuine, common reasons to extract a service: a specific part of the system needs to scale independently and dramatically differently from the rest (an image-processing pipeline under heavy, bursty load while the rest of the application is lightly used), a specific part needs a different technology entirely (a machine-learning inference service better suited to Python than PHP), or a specific part needs to be owned and deployed independently by a separate team with its own release cadence. "It feels more modern" or "big companies do this" are not, on their own, good reasons — the operational cost of microservices is real and substantial, and should be justified by a concrete, current problem.
The Modular Monolith: A Middle Ground
Before reaching for full microservices, a modular monolith — one deployable application internally organized into clearly bounded modules with disciplined, well-defined interfaces between them — captures much of the maintainability benefit people associate with microservices, without the distributed-systems cost:
// app/Modules/Billing/BillingService.php
namespace App\Modules\Billing;
class BillingService {
public function chargeCustomer(Customer $customer, Money $amount): Charge { /* ... */ }
}
// other modules call through this interface, never reaching into Billing's internal models directlyThis structure also makes a later, genuine extraction into a real separate service meaningfully easier, since the module boundary and its interface already exist — extraction becomes "move this code and put a network call where the in-process call used to be," not a from-scratch redesign of how the pieces talk to each other.
What Breaks When You Split Too Early
Distributed transactions become a real, hard problem the moment data that used to live in one database transaction now spans two services — eventual consistency and compensating actions (sagas) replace the simple guarantee a single database transaction used to provide for free. Network calls between services introduce latency, partial failure modes, and a need for retries and circuit breakers that simply do not exist for in-process method calls within a monolith. Local development gets harder, since running "the application" now means running and coordinating several services rather than one, often pushing teams toward heavier local tooling (Docker Compose stacks) just to develop at all.
A Sane Migration Path, If You Do Need to Split
Identify a service boundary with the clearest, narrowest interface to the rest of the system (billing, notifications, and search are common first candidates, since they tend to have naturally narrow interfaces already). Extract that one service first, and live with it in production for a meaningful period before extracting a second one — resisting the temptation to plan a full microservices decomposition up front and execute it in one large effort, which is a common way such migrations stall or fail outright partway through.
Closing Thought
The right architecture is the one that matches your actual current problems, not the one that matches what the largest, most-talked-about tech companies use for their very different scale and organizational structure. A disciplined modular monolith serves the overwhelming majority of applications well, often indefinitely, and the cases that genuinely benefit from microservices usually reveal themselves clearly through specific, concrete scaling or organizational pain — not through a sense that the architecture should look more sophisticated.
Data Consistency Across Service Boundaries
In a monolith, an operation touching multiple entities (placing an order, deducting inventory, charging a payment) can happen within a single atomic database transaction — either everything succeeds or everything rolls back together. Split across services, that same operation now spans separate databases with no shared transaction, requiring a different consistency strategy entirely: typically a saga pattern, where each step publishes an event that triggers the next, and a failure partway through triggers compensating actions (refunding a charge, restoring inventory) rather than a clean automatic rollback.
// saga-style compensation, not a database rollback
class OrderSaga {
public function onInventoryReservationFailed(InventoryReservationFailed $event) {
$this->paymentService->refund($event->orderId);
}
}Observability Becomes Mandatory, Not Optional
A single slow or failing request in a monolith is debuggable with a stack trace and a debugger attached to one process. The same request spanning five microservices requires distributed tracing (a shared request ID propagated across every service call) just to reconstruct what actually happened across the chain of calls — without this instrumentation built in from the start, debugging a cross-service issue becomes a slow, manual correlation exercise across multiple separate log sources that were never designed to be read together.
Organizational Reasons to Split, Separate From Technical Ones
Conway's Law observes that a system's architecture tends to mirror the communication structure of the organization that builds it. A genuinely large engineering organization with several independent teams, each wanting to own and deploy their part of the system on their own schedule without coordinating every release with every other team, has an organizational reason to split services that exists independently of any purely technical scaling argument — though this reasoning applies specifically at a scale of engineering organization most companies, even successful ones, never actually reach.
The Strangler Fig Pattern
Rather than a risky big-bang rewrite, the strangler fig pattern routes specific, narrow slices of traffic to a new extracted service while the monolith continues handling everything else, gradually "strangling" the monolith's responsibility for that specific area piece by piece as confidence in the new service grows. This keeps the system in a working, deployable state throughout the migration, rather than requiring a long, risky period where neither the old nor the new system is fully functional and the team is committed to finishing before anything can ship again.
Case Study: A Startup That Built Microservices for a Problem It Did Not Have
A five-person startup, inspired by conference talks from companies operating at vastly larger scale, built their MVP as eight separate microservices from day one, reasoning it would "scale better later." In practice, with five engineers, every feature touched three or four services, requiring coordinated deploys across all of them for nearly every change, and local development required running eight separate processes just to test a single feature end to end. Eighteen months in, with the product still finding its market fit, the team made the unusual but correct call to consolidate back into a single modular monolith, citing the coordination overhead as actively slowing down the exact kind of fast iteration an early-stage startup most needs. The lesson is not that microservices are wrong, but that they solve organizational and scaling problems a five-person team simply does not have yet, while imposing real costs that team felt immediately.
A Glossary for This Topic
Bounded context: a clearly defined area of business logic with its own model and language, a common unit of decomposition when splitting a system into services. Saga pattern: a way of managing a multi-step process across services using a sequence of local transactions and compensating actions instead of one distributed transaction. Strangler fig pattern: incrementally routing specific functionality from an old system to a new one until the old system's relevant responsibility is fully replaced. Distributed tracing: tooling that follows a single request across multiple services using a shared trace ID, used to debug cross-service issues.
Frequently Asked Questions
Is a modular monolith just microservices without the network calls? Conceptually similar in terms of internal boundaries and discipline, but meaningfully simpler operationally — one deployment, one database transaction scope, and no need for service discovery or distributed tracing infrastructure.
How many engineers does a team need before microservices make sense? There is no fixed number, but the organizational argument (independent teams needing independent deploy schedules) typically does not apply meaningfully below dozens of engineers organized into several genuinely separate teams.
Can I mix the two approaches within one system? Yes — many real systems run a primary monolith alongside a small number of deliberately extracted services for the specific cases (different scaling needs, different technology requirements) that genuinely justify the added complexity.
Step-by-Step: Deciding Whether to Extract a Service
Step one: name the specific, current pain the split is meant to solve — a concrete scaling bottleneck, a specific cross-team deploy-coordination problem — rather than a general sense that microservices are the more modern approach. Step two: check whether a modular monolith with clearer internal boundaries would solve the same pain without the distributed-systems cost, and rule that out specifically before committing to a real service split. Step three: if a split is genuinely justified, pick the single candidate with the narrowest, cleanest interface to the rest of the system, not the most architecturally interesting one. Step four: build observability (distributed tracing, correlated logging) before the split goes to production, not after the first hard-to-debug cross-service issue forces it. Step five: use the strangler fig pattern to migrate traffic gradually, keeping the system deployable and functional throughout. Step six: live with one extracted service in production for a meaningful period before considering whether a second split is genuinely warranted.
A Comparison Table: Splitting Strategies at a Glance
Big-bang rewrite into microservices: highest risk, longest period without a fully working system, rarely recommended regardless of team size. Modular monolith: lowest risk, captures much of the maintainability benefit, the right default starting point for most teams. Strangler fig incremental extraction: moderate risk, system stays functional throughout, the right approach once a genuine, specific service-extraction need has been identified.
Security Considerations Checklist
Service-to-service communication needs its own authentication (mutual TLS or service-specific tokens), since an internal network is not automatically trusted territory — a compromised service should not be able to freely call every other internal service with no further verification. Each service should validate its own inputs independently rather than assuming an upstream service already sanitized everything, since trusting upstream validation blindly is exactly the kind of assumption that breaks down the moment a new, unreviewed caller is added later. Secrets and credentials should be managed through a centralized secrets store accessible per-service with least-privilege scoping, not duplicated and hardcoded independently across every service's own configuration.
Accessibility Considerations
Architecture choices are largely invisible to end users and have little direct accessibility impact, but the operational complexity of microservices can indirectly affect it — a slower, more fragile deployment pipeline across many services can mean accessibility fixes (a contrast issue, a missing ARIA label) take longer to reach production than they would in a simpler, faster-to-deploy monolith, an indirect but real cost worth weighing.
How This Plays Out at Different Scales
A small team is almost always best served by a monolith or modular monolith, full stop. A mid-size company with a few specific, genuine scaling or technology-fit needs benefits from selectively extracting just those specific services while keeping the bulk of the system as a monolith. A large organization with many independent engineering teams may genuinely need a broader microservices architecture, but even there, successful organizations tend to keep services larger and fewer than the most extreme decomposition examples often cited in conference talks.
What to Do When You Inherit a Half-Finished Microservices Migration
Inheriting a system partway through an abandoned or stalled microservices migration — some functionality split out, the rest still in the original monolith, with no clear ongoing plan — is more common than a clean migration success story. Rather than resuming the original decomposition plan by default, reassess from scratch whether the already-extracted services are actually earning their operational cost in practice, and seriously consider folding a poorly-justified extracted service back into the monolith if it is not delivering real value, rather than treating "we already started, so we should finish" as sufficient justification on its own to keep pushing further in a direction that may not have been right to begin with.
Final Checklist Before Extracting a Service
A specific, current, concrete pain point justifies the split, not a general preference for the architecture. A modular monolith approach has been explicitly ruled out as insufficient for the actual problem. Distributed tracing and correlated logging are in place before the split ships. A saga or equivalent compensation strategy is designed for any operation that previously relied on a single database transaction. The team has committed to living with one extraction before planning a second.
Closing Thought, Revisited
The most expensive mistake in this space is rarely choosing the wrong architecture outright — it is choosing an architecture that does not match your team's actual current size, problems, and organizational structure, and only discovering the mismatch after a meaningful amount of work has already gone into it. Naming the actual problem before naming the architectural solution is the discipline that keeps a team from paying complexity costs for benefits they were never positioned to realize in the first place.
The Cost of Premature Optimization for Scale You Do Not Have
Designing for a hypothetical scale of millions of users when a product currently has a few hundred is a specific, common variant of premature optimization that microservices discussions invite more than most architectural decisions, precisely because the scaling story sounds so concrete and compelling. The actual cost is not abstract — it is the real engineering hours spent building and maintaining distributed-systems infrastructure that could have gone into the product features that determine whether the product reaches meaningful scale in the first place.
Versioning APIs Between Services
Once two services communicate over a network boundary, changing the contract between them (renaming a field, changing a response shape) can no longer happen as a single atomic code change the way it would within a monolith — the consuming service and the producing service are deployed independently, so a breaking change needs a transition period where both old and new contract versions are supported simultaneously, with consumers migrating before the old version is finally retired.