Monolith versus microservices isn't a binary โ it's a spectrum with a big ball of mud at one end
and a fleet of independently deployed services at the other, plus several structured stops in between that let
you get the benefits of separation without the operational tax.
๐งญ
How to read this
This chapter is one long spectrum. It opens with the two poles โ monolith and microservices โ and their honest
trade-offs, grounded in real companies (Stack Overflow, LinkedIn, Uber, Segment, Istio) that moved in both
directions. Then it pivots to a key insight: many teams reach for microservices only to escape a big ball
of mud, and there are cheaper structural fixes โ layered, modular-monolith, and hexagonal architectures โ before you
pay the distributed-systems tax. It closes with the communication styles (blocking vs. non-blocking,
event-driven) that make those structures work. Anything marked [+]
is context I added; everything else is rewritten from the fifteen teaching lessons.
01
The monolith puts everything in one box
An application is a monolith when its entire code lives in a single codebase โ one self-contained,
tightly coupled unit.
Take a social network like Facebook: user posts, comments, groups, marketplace, ads, photo storage, live
streaming, a recommendation engine. In a monolith, every one of those features is coded together in the same
repository, tightly coupled, rather than each having its own dedicated service.
The upside is right there: monoliths are simple to build, test, and deploy. There's no overhead of managing
infrastructure for many services or wiring up inter-service communication.
Figure 1 โ
A monolith: every feature compiled and deployed together, backed by one shared database. Original diagram
after the course's Illustration 1.33.
Starting monolith, growing into microservices
Teams often start with a monolith intending to break out into microservices later. LinkedIn
began as a monolith called "Leo" wired to a handful of databases, then moved to a distributed architecture.
Uber
started with a single repo holding all its logic โ matching drivers and riders, background checks, billing,
payments โ and hit the classic monolith walls as it grew: scaling pain, tight coupling, redeploying everything
for one small change, and the constant risk of a change cascading into unrelated features, forcing heavy
regression testing.
๐ธ
"Start monolith, migrate later" is itself a trade-off
Both LinkedIn and Uber transitioned when cloud and microservices weren't yet the norm and traffic was
limited. Today, refactoring a tightly coupled monolith into services carries real cost โ dismantling and
re-implementing features takes serious time and resources. If you already know you have many distinct
features and expect rapid growth, picking loosely coupled microservices from the start can be the wiser bet.
If requirements are simple, microservices are overkill.
02
When a monolith is actually the right call
The monolith's single virtue is simplicity
โ one repo to develop, test, deploy, monitor, and manage. But that single box has real costs:
Cost of the monolith
What it means
Painful continuous deployment
Even a tiny change in one layer or feature forces a redeploy of the entire application.
Heavy regression testing
Because everything is tightly coupled, a small change in one feature can break others โ so the whole
app must be re-tested after each deploy.
Single point of failure
A bug in any one feature can bring the entire application down.
Scalability limits
You can't scale one feature on its own; any change applies to the whole app, and management gets
harder as the code grows.
No heterogeneous tech
Mixing languages in one codebase is messy or impossible โ combining Java and NodeJS in a single
codebase is, optimistically, "tricky."
Often not cloud-ready
Legacy monoliths tend to hold state in static variables; a cloud-native app needs to be stateless for
consistent behavior.
Pick a monolith when the app is genuinely simple
and won't need serious expansion soon โ a to-do app, a mostly read-only sports news app, an internal
tax-calculation tool.
๐
There's no formula โ and Stack Overflow proves it
There is no rule that use case X demands architecture Y; it's always trade-offs. Stack
Overflow
started as a monolith and remains one โ serving 4 billion requests/month (3,000 req/s peak) and 800M SQL
queries/day (8,500/s peak). At that scale, monolith still works for them.
03
Microservices split the app by feature
In a microservices architecture, each feature of a large service is deployed separately as a small,
loosely coupled service, and these work together to form the whole.
It's the direct application of two design principles: single responsibility
(each service runs one feature) and separation of concerns
(each is isolated from the others).
That separation pays off across the board: cleaner maintenance, easier feature development and testing, and
independent deployment of each module. Large projects can hand each service to a dedicated team with minimal
friction. Scaling is built in โ a service that needs more capacity scales independently without touching the
others. And critically, each microservice ideally has its own database, which removes shared single points of failure and bottlenecks.
Figure 2 โ
Microservices: each feature is its own service with its own database (SQL or NoSQL), reached through an API
gateway. Dashed boxes mark service boundaries. Original diagram after Illustration 1.35.
04
When microservices earn their cost
โ No single point of failureLoosely coupled โ a few services can go down and the app as a whole stays up.
โ Heterogeneous techServices talk over a REST API gateway, so you can mix Java, Python, Ruby, NodeJS, and polyglot
persistence (SQL + NoSQL together).
โ Independent, continuous deploysDedicated teams per service, each scaled and shipped without impacting others.
โ ๏ธ Management complexityA distributed environment needs a node manager (e.g. Apache ZooKeeper), distributed tracing, and often a
dedicated team to run it.
โ ๏ธ Weak consistencyStrong consistency across services is hard; you usually get eventual consistency because of the
distributed design.
๐ฏ Best for complex, fast-moving appsA social network (messaging, live chat, video, photos, likes/shares) is the canonical fit โ features
evolve independently with little regression testing.
So there are three ways forward: pick a monolith, pick microservices, or start monolith and later scale out
into microservices. Which one depends entirely on the use case โ the course's advice is to keep things simple
and understand requirements thoroughly before deciding.
05
The trade-offs that actually decide it
Beyond the pro/con lists, two forces tend to drive the real decision.
Fault isolation
With microservices, a glitch is contained. You fix the issue in the one service without scanning the whole
codebase to find and repair it โ that containment is
fault isolation.
Development team autonomy
In a monolith, once the number of developers and teams on one codebase grows past a point, productivity drops.
As the codebase grows, compile time and test runtime grow with it โ because the entire
codebase recompiles after any change, not just your module. Worse:
Another team's change can directly break your feature, so thorough regression testing is required on every
push.
Everyone may have to stop working while a change goes to production.
Your change may need approval from other teams sharing the codebase โ a bottleneck.
Microservices flip this: dedicated teams own their codebases outright, with full development and deployment
autonomy and separate pipelines. If you need to move fast, launch many features, and scale โ microservices are a
good bet. But that delight comes with cost: distributed logging, monitoring, inter-service communication,
service discovery, alerts, tracing, dedicated build/release pipelines, health checks, and often custom tooling
written from scratch. There's no perfect solution โ only clarity about your use case.
06
The round trip: Segment and Istio went back
The most instructive trade-off stories are the companies that reversed course.
Segment: monolith โ microservices โ monolith
Segment, a customer data platform, ingests hundreds of thousands of events per second and routes them to 100+
server-side destinations (APIs and webhooks) via message queues. The journey:
The monolith.
One API ingested events and pushed them to a single distributed queue, which forwarded payloads to
destinations. Failed events were retried โ so the one queue filled with both new and retried events,
eventually flooding and delaying delivery.
The split to microservices.
They gave every destination its own microservice with its own queue. A flooded queue no longer hurt other
destinations โ clean fault isolation, better throughput.
The complexity wall.
As destinations grew, each service+queue had different throughput and load patterns; no single scaling
policy fit. Autoscaling still needed manual per-service CPU/memory tuning. More queues meant more
maintenance resources.
Back to a monolith.
They reverted, combining all the per-destination queues into a single service called
Centrifuge, giving up fault isolation to reclaim manageability.
Figure 3 โ
Segment's round trip: one flooded queue โ per-destination isolation that grew unmanageable โ recombined into
the Centrifuge monolith. Original diagram after Illustrations 1.36โ1.37.
Istio
Istio
โ the open-source service mesh for connecting, securing, controlling, and observing microservices โ itself moved
from microservices back to a monolith, saying it let them deliver value and hit their goals. It's cited as an
example of when not
to do microservices.
07
The big ball of mud โ the thing everyone's fleeing
The big ball of mud (aka spaghetti code) is code with no definite structure โ tightly coupled, hard to
maintain, hard to refactor.
It's a software-design anti-pattern that shows up when delivery speed is prioritized over code quality, piling
up massive technical debt.
Picture an online food-delivery service: taking orders, routing them to restaurants and drivers, payments,
memberships, inventory. In spaghetti form, all of it is coded together, tightly coupled โ no layers (controller,
service, DAO), no SOLID principles, no interfaces, no modular design. Adding a feature means first
reverse-engineering the code flow, then changing it and hoping nothing breaks โ an endless cycle of bugs
spawning bugs, and eventually financial loss.
Figure 4 โ
The big ball of mud (tangled, tightly coupled) versus a structured decomposition. Original diagram after
Illustration 1.38.
How code turns to mud
The causes are cultural as much as technical: no time invested in design early on; inexperienced developers
with little design knowledge; many devs over years each coding in their own style with no standard structure
(this is where frameworks like Spring help enforce consistency); always choosing delivery speed over quality; no
team code reviews led by seasoned devs (if it worked, it got merged); and no regular refactoring, quality
checks, or test coverage.
The fix the course prescribes: don't stuff everything in one layer โ use a layered architecture.
08
Layered architecture: give each job a floor
Layered architecture (the "lasagna code") splits code into layers โ typically controller, service, and
data-access โ each with one role, communicating through interfaces to stay loosely coupled.
The controller handles requests, the service layer runs business logic, the DAO talks to the database. Each
layer does its job without worrying about the others; it's the separation of concerns
principle in practice. A change in one layer barely touches the rest, which keeps the code maintainable,
testable, and extendable. In Java Spring enterprise projects, this is the default pattern.
Figure 5 โ
Business features run as vertical slices across the controller/service/DAO layers. A request flows down one
feature's column and the response back up. Original diagram after Illustrations 1.39โ1.40.
But layers aren't full isolation.
Business features cross-communicate, and in complex projects that cross-talk can turn the code back into a big
ball of mud. The escape hatches: move to microservices, or implement a modular monolith. The course also lists non-structural safeguards โ seasoned-dev code reviews as a rule, educating juniors on
design, practicing domain-driven development, and fixing root causes instead of duct-taping bugs.
09
The modular monolith: the middle ground
A modular monolith is a monolith organized into loosely coupled modules split by business feature, each
with its own layers, rather than everything coded together.
It sits squarely between the monolith and microservices. If your only reason to adopt microservices is to escape
the big ball of mud, this gets you most of the benefit without the distributed-systems overhead.
Modules are drawn along business functionality โ inventory in one module, orders in another, payments in
another โ and they can even connect to different databases rather than sharing the single one a classic monolith
uses. The payoff mirrors microservices: code reusability, easier maintenance and extension, better
comprehension, observability, lower complexity, and easier testing.
๐
The one thing it can't do
The modules are separated logically, not physically
โ so unlike microservices, they cannot be deployed independently. If you need physical separation and
independent deployment, you must move to microservices.
Modularity and domain-driven design
The modular monolith is an implementation of domain-driven design (DDD)
โ structuring code to align with the business domain. In enterprise projects, domain experts and technical staff
model the system together, defining bounded contexts
(logical boundaries in a domain with consistent rules โ e.g. inventory management is one bounded context, user
management another) and converting those contexts into modules. Once built, modules have dependencies on each
other โ inventory talks to orders, orders talk to payment โ which raises the question of how
they communicate.
10
How modules talk: sync or async
Modules in a modular monolith communicate one of two ways.
Synchronous โ through interfaces
A module calls another only through the interface
that module exposes, with no knowledge of its implementation. The interface is the entry point, so a module can
swap its implementation anytime without breaking callers โ loosely coupled by design. (This is also how
hexagonal architecture, coming next, does synchronous communication.)
Asynchronous โ through an event bus
Async communication is event-based, via an in-memory event bus or a message broker like Kafka. A module publishes events to the bus; other modules subscribed to it read those events. The bus is a central
collection point for events across the app, letting modules interact with no direct coupling
between them. Guava's EventBus
is one example. This is the publish-subscribe pattern.
Figure 6 โ
Synchronous (direct call through an interface) vs. asynchronous (publish/subscribe through an event bus or
broker). Original diagram after Illustrations 1.42โ1.43.
11
Hexagonal architecture: business logic at the core
Hexagonal architecture โ aka Ports and Adapters โ keeps business logic isolated at the center and pushes
all input/output to the edges.
It's one way to implement a modular monolith's synchronous communication. Three components:
Domain
โ the business logic, sitting at the core; external entities never touch it directly.
Ports
โ interfaces/APIs; all input enters through a port.
Adapters
โ the implementations of those ports; they convert data from a port into a form the business logic can
process.
The hexagon shape means nothing โ it's just a visual convention. The name comes by analogy to computer ports
(the interface to external devices) and adapters (which convert the port's signals for the chips inside).
Figure 7 โ
Ports & adapters. Inbound ports carry requests from the application layer into the domain; outbound ports
carry the domain's calls out to infrastructure. Original diagram after Illustrations 1.44โ1.46.
Why it beats spreading logic across layers
In plain layered architecture, business logic tends to leak across layers โ even into the UI and database stored
procedures โ making things tightly coupled and hard to test or refactor. Hexagonal keeps all
business logic at the core, so a change to it doesn't force refactoring every layer. It still has
controller/service/DAO pieces, but organizes them under three high-level layers: application
(UI, REST controllers), domain
(the model plus the port interfaces), and infrastructure
(database, messaging server, and the adapters implementing the ports). Ports/adapters handling incoming traffic
are inbound; those handling outgoing traffic to infrastructure are outbound.
Upsides:
business logic can be unit-tested in isolation (no need for integration tests or cloud deploys just to test
logic โ integration tests are better spent on the surrounding layers), and the code stays loosely coupled,
extendable, and therefore scalable.
12
Blocking vs. non-blocking โ the async foundation
The asynchronous, event-based module communication above rests on a lower-level distinction. Modern frameworks
like NodeJS, Play, Tornado, and Akka gained popularity over traditional tech (Java, PHP) for real reasons โ
starting here.
Blocking (synchronous)
Non-blocking (asynchronous)
Behavior
Execution pauses at a call and waits for it to return before moving on.
Execution doesn't wait โ it moves to the next line immediately, whether or not the call has returned.
Trade-off
Predictable; the default in typical CRUD apps.
Slightly less consistent (the called function might error or return nothing, yet the next line still
runs), but frees the flow.
Best for
Straightforward request/response work.
IO-intensive operations โ disk, network, hardware โ i.e. lots of events.
๐งฉ
Events, defined
An event is anything from a tweet to a button click, an HTTP request, an ingested message, or a variable
change. When events happen very frequently, that's a stream. In web apps, "IO" essentially means events โ many IO operations means many events over time.
13
Event-driven architecture: react to the stream
Event-driven architecture (also called reactive or non-blocking) is built to process asynchronous
streams of events โ the system continually monitors the stream and reacts.
The application becomes inherently asynchronous.
At the service level
Non-blocking technologies โ NodeJS, and Java-ecosystem frameworks like Play, Akka, and Spring Reactor โ let a
service handle a large number of concurrent connections with minimal resources. They're built to run on a
cluster and handle large-scale concurrency without the developer wrestling with threads, locks, or out-of-memory
issues from high IO. Doing reactive work with traditional thread-based frameworks is painful precisely because
of shared mutable state and locks. NodeJS
is single-threaded and non-blocking with an event-loop architecture, ideal for IO-intensive work โ but a poor
fit for CPU-intensive
tasks like graphics rendering or ML. Every tech has its use case; non-blocking doesn't make traditional tech
obsolete.
At the system level
For streams across many services โ changing stock prices, e-commerce transaction events, changing game states โ
an external message broker
(like Kafka) connects the services and routes events between them. In the vocabulary of event-driven systems:
modules that publish events are publishers/producers, modules that consume them are consumers, and the broker in the middle is the event router. If you split modules into microservices, users become the event producers and the microservices become the
event consumers, with all events flowing through the router.
Figure 8 โ
System-level event-driven architecture: producers โ event router (broker) โ consumer services. Original
diagram after Illustrations 1.48โ1.49.
Why it's worth it
Loose coupling.
Say a customer joins a group when they place an order. If services were tightly coupled and the membership
service were momentarily down, the whole order flow would fail. With event-driven design, the broker
persists
the membership events while that service is down; when it recovers, it pulls and processes them โ so the
business doesn't lose the customer. Parallel processing.
Because communication is async through queues, services run their work in parallel without waiting on each
other, and if one goes down mid-task, its events sit safely in the queue without affecting the rest.
14
Recap
MonolithOne codebase, tightly coupled. Simple to ship; but SPOF, hard to scale per-feature, painful redeploys.
Great for simple apps (Stack Overflow scaled one).
MicroservicesFeature-split, loosely coupled, own databases, independent deploys, polyglot tech. Cost:
distributed-systems complexity and eventual consistency.
The decision is a trade-offFault isolation and team autonomy pull toward microservices; management overhead pulls back. Segment and
Istio both reversed course.
Big ball of mudStructureless spaghetti from prioritizing speed over quality. The thing most teams are really
fleeing.
Layered โ modular โ hexagonalStructural fixes that give separation without the distributed tax. Modular monolith is the middle
ground; hexagonal isolates business logic at the core.
Event-driven glueNon-blocking + a broker (Kafka) let modules/services publish and subscribe, decoupled and parallel,
surviving each other's outages.
๐ก
The single idea underneath it all
Coupling is the enemy, but distribution is a tax. Every architecture here is a different answer to "how
loosely coupled do I need to be, and how much operational cost will I pay for it?" You can buy decoupling
with structure inside one process (layered, modular, hexagonal) long before you buy it with a fleet of
services โ and the companies that forgot this, like Segment and Istio, paid the tax and then asked for a
refund.
15
Further reading
The lessons cite several external links. Below are the ones that resolve live today, grouped by topic. Several
other cited links โ a LinkedIn scaling article, an Uber scaling write-up, a Node.js event-loop guide, and a
Stack Exchange performance page โ returned errors or 404s and are omitted.