Around 2016, with the popularity of Spring Cloud, a common architecture diagram appeared in the Chinese tech community: a box in the center labeled "Business System," surrounded by a dozen smaller boxes — user service, order service, payment service, product service, message service, log service, config center, registry, API gateway... Each small box was a complete Spring Boot project with its own CI pipeline, Dockerfile, and deployment scripts.
Spring Cloud solved a real set of problems — service discovery, configuration management, load balancing in distributed systems. But the maturity of the tooling had a side effect: architectural decisions started being driven by tool capabilities. "Since we have this toolset, why not split into microservices?" became the default thinking. As for why to split, where to draw module boundaries, and how much operations cost would increase — these questions were deprioritized.
In the years that followed, Go rapidly gained ground in backend development, and Kubernetes became the de facto container orchestration standard. The industry's attitude toward service splitting became noticeably more measured. Not because there's something wrong with microservices per se — but because the inherent complexity of distributed systems became more widely understood. Going from a blind rush to split everything to "calculate the costs first" was itself an important industry lesson.
A Good Monolith Is Already Modular
When people discuss monoliths, many picture a tangled mess: no module boundaries, arbitrary cross-package imports, changing one line of code with unknown ripple effects. But that's not an inevitable property of monolithic architecture — it's the result of absent architectural discipline.
A monolith with basic architectural discipline naturally organizes by business capability — order module, user module, payment module, each with its own internal implementation and clear interfaces for cross-module communication. This is standard software engineering practice. Go uses internal directories to constrain visibility; Java uses package structure and access modifiers. Different languages, same approach.
These constraints aren't decorative. When an order module's internal DAO is marked as unexported, a user module developer can't directly import it. They can only call the order module's exposed interfaces — a GetOrderStatus function, for instance. This restriction compresses inter-module coupling from "arbitrarily access any internal implementation" to "depend only on the published API." How internal data structures are designed, how database tables are laid out — as long as the external interface doesn't change, callers are unaffected. This is the fundamental value of encapsulation. Not new, but frequently forgotten when discussing monoliths.
If a team can't draw module boundaries clearly inside a monolith, the microservice boundaries they produce will likely be wrong too. Splitting freezes wrong boundaries into network protocols, where fixing them is far more expensive. Boundary quality doesn't depend on the number of deployment units.
A normally growing system typically follows this path: first run the module boundaries clearly within a single process, then extract a module as an independent service when needed. For many systems, staying in this form long-term is perfectly sufficient.
To Split or Not: The Real Bills
Looking at the differences between monolith and microservices across several key dimensions. These differences don't decide "which is better" — they determine what you're signing up for.
Latency. In-process calls and network calls are usually not in the same order of magnitude. No matter how deep the call chain in a monolith, additional latency remains low. In a microservice architecture, latency accumulates with chain depth. A user request arrives, passes through gateway, user service, order service, payment service, notification service — five hops, even at 5ms each, that's 25ms. Not fatal for most systems, but it's a continuously growing tax, and there's no day when you can declare "the latency problem is solved" — it only gets harder to manage as service count grows.
Transactions. In a monolith, cross-module operations are a single database transaction with ACID guarantees. In microservices, cross-service operations require distributed transactions or Saga — introducing compensation, retry, idempotency, eventual consistency. The data consistency model shifts from strong to eventual, and the impact of this change on business logic is often underestimated. Payment succeeds but order creation fails; refund logic triggers an exception but the user already received a refund success notification — these problems don't exist in a monolith but require additional reconciliation, compensation, and manual intervention in microservices.
Debugging and troubleshooting. In a monolith, one request is traceable end to end. In microservices, you stitch together multiple log files, dependent on the coverage and correctness of distributed tracing. Tracing itself is infrastructure that needs maintenance — collection, transport, storage, query — each link can break. Confirming "is tracing working correctly?" becomes its own debugging step when troubleshooting a production issue.
Deployment and operations. A monolith is one CI/CD pipeline, one image, one release. Microservices mean independent pipelines and deployment configs per service. Automation reduces operational burden, but cognitive overhead doesn't disappear — understanding the relationships between ten deployment units, release ordering, API compatibility matrices is itself a cost. When service A's v2 requires service B's v3 to function, releases are no longer independent, and the version dependency graph needs managing.
Testing. Monolith end-to-end tests: just start the whole application. Microservice end-to-end tests: either maintain a staging cluster, spin up all services with docker-compose in CI (unstable), or rely heavily on contract testing and mocks. All three approaches have costs; none is "configure once and forget."
Team collaboration. Microservices' most tangible advantage: independent development, independent deployment, no mutual blocking. This advantage holds when module boundaries are clear and stable enough, and when team structure aligns with system boundaries. If boundaries have already been run cleanly in a monolith and teams are organized by module, extracting one module as an independent service is low-cost. The team collaboration benefit of microservices usually materializes only after modular design is done well — and once modular design is done well, microservices become an optional extra step, not a necessity.
Across these six dimensions, the microservice advantages hardest to replace with a monolith include: independent scaling, independent fault isolation, independent deployment cadence, and aligning system boundaries with organizational boundaries at large team scale. Everything else is tradeoff. If the system doesn't have these needs, splitting into microservices mainly brings additional cost.
When to Split
Signals that actually occur are more reliable than "planning" for when to extract a module as an independent service:
Load divergence. A module's load characteristics differ significantly from others, and this is visible in monitoring data — not "might in the future" estimates. For example, the order service's CPU and memory curves decouple from the user service during peak hours; the benefit of independent scaling is calculable from monitoring charts.
Independent delivery cadence. A module's change frequency far exceeds others, and it's already been slowed by the overall release cadence — this has already happened, not planned acceleration. For example, the payment module needs weekly releases, but because it's bundled with other modules, every release waits for the entire system's regression tests.
Fault isolation. A module's failure shouldn't take down the entire system. But if this module is the core business engine, its failure takes down the system anyway — extracting it doesn't increase availability, and introduces network communication as a new failure point. The value of fault isolation depends on whether this module's availability requirements are genuinely higher than the rest of the system, and whether its failure modes can actually be isolated through independent deployment.
Team size. The team is large enough that collaborating in a single codebase has become inefficient. This threshold is higher than many imagine — a monolith with clear module boundaries can support a sizable team. Ten people with ten microservices means each service has a bus factor close to one; if anyone leaves, their service becomes a no-go zone.
These conditions aren't absolute. But if none of the signals have appeared, splitting a monolith into microservices is most likely pre-paying complexity for needs that haven't materialized.
Physical Isolation Isn't Free
A common argument for splitting: physical isolation makes boundaries unbreakable. Without it, boundaries depend on discipline, which doesn't hold.
Physical isolation does prevent "accidentally importing something you shouldn't." But under microservices, boundary violations don't disappear — they take different forms: interface semantics silently change without downstream knowing, field meanings shift, exceptions stop being thrown. These problems are harder to catch with static analysis because they exist at the protocol level, not the type level. In a monolith, changing a function signature lets the compiler tell you all affected call sites in seconds. In microservices, changing a REST API response field might be discovered days later in integration tests — or worse, in production via alerts.
Conversely, monolith compile-time checks and static analysis rules enforce module access restrictions on every build. This mechanism is repeatable and deterministic. Go's internal packages, Java's module system, ArchUnit-style static analysis tools — these can intercept dependency violations in CI. After one-time configuration, zero ongoing human cost.
There are scenarios where network isolation is necessary — compliance requirements mandating physical data isolation, for instance. In these cases, microservices are the right choice. But for most systems, the core need is simply preventing the order module from directly using the user module's internal implementation, and compile-time checks accomplish that. Betting boundary quality on compilers and static analysis is at least more verifiable than betting on "others can't access my database over the network."
Summary
Whether to split into microservices isn't a question of "modernity." It's first a calculation: does exchanging network communication costs for physical isolation benefits make sense at the current stage?
For many systems, the answer at the current stage is no. Not because microservices are bad, but because the constraints haven't reached the point where distributed complexity is warranted. Run the module boundaries clearly in the monolith, design stable interfaces, organize teams by module — once these are done, the day you actually need independent scaling or delivery cadence, extracting a module costs very little. Because you already know what the interfaces look like, who owns the data, and what the dependency directions are.
Before that day arrives, continuing to run a well-modularized monolith isn't technical conservatism — it's refusing to pre-pay for needs that haven't materialized.