Nine parts in, we have one well-factored service: clean boundaries inside, containers and CI/CD outside. The fashionable next step is to cut it into microservices. This part is the honest version of that conversation: what microservices actually buy, what they actually cost, how to find real service boundaries, and why the modular monolith we've been building is not a consolation prize but the best position on the board.

What the architecture actually trades
Microservices exchange in-process complexity for distributed-systems complexity. Every benefit and cost follows from that one trade.
What you gain:
- Independent deployment. Team A ships payments without waiting for team B's half-finished search feature. This is the benefit — organizational, not technical.
- Independent scaling. The resize-images service runs 40 replicas; the invoicing service runs 2.
- Failure isolation (potentially). A crashed recommendation service degrades the page instead of downing it — if callers handle its absence.
- Technology freedom per service — occasionally valuable, frequently abused.
What you pay:
- A method call becomes a network call — latency, timeouts, retries, partial failure, versioned contracts.
- Transactions stop being a
SaveChangesAsync. Consistency across services means sagas, outboxes, and eventual consistency — and explaining eventual consistency to the business. - Debugging spans machines. "Why is checkout slow?" now requires distributed tracing (part 10 exists for a reason).
- Operational multiplication. N services × (pipeline + dashboards + alerts + on-call surface).
The rule of thumb that survives contact with reality: microservices solve team-scaling problems. If one team owns the whole system, you pay the distributed tax and collect almost none of the benefit.
Boundaries: cut along the domain, not the layers
The worst split is the one that mirrors technical layers (an "API service" calling a "business service" calling a "data service") — that's a monolith with network cables through it. Real boundaries come from the business domain — DDD's bounded contexts: Catalog, Ordering, Identity, Notifications. Two tests for a candidate boundary:
- Data ownership. Each service owns its tables exclusively; others get data via API or events, never via a shared database. If two candidate services constantly need each other's rows in one transaction, they're one service.
- Conversational autonomy. Can this service handle its main use cases without synchronously calling three others? A service that can't is a distributed function, not a microservice.
The modular monolith: same boundaries, one process
Here's the part the conference talks skip: you can have domain boundaries now, without the network. One deployable, modules with schema-per-module data ownership, communication through in-process interfaces and events:
GeeksArray.Web (one container)
├── Modules/Blogs → schema "blogs"
├── Modules/Identity → schema "identity"
├── Modules/Training → schema "training"
└── Modules/Tags → schema "tags"
That's not hypothetical — it's the actual architecture of the platform serving this article. Modules reference each other only through public service interfaces; each module owns its PostgreSQL schema; cross-module workflows go through events. The whole thing deploys as one container from parts 6–8.
The payoff: boundary mistakes are refactors, not migrations. Move a class between modules and the compiler guides you. Get the same boundary wrong across two microservices and you're versioning APIs and double-writing data for a quarter. Practice drawing boundaries where redrawing is cheap; extract to a network service when a specific pressure demands it.
When extraction is actually justified
Extract a module into a service when you can name the concrete forcing pressure:
- A module needs 10× the scale of the rest (resize workers, ML inference).
- A module needs a different runtime or language.
- A separate team now owns it and needs an independent release cadence.
- Compliance isolation (payment data behind its own perimeter).
Because the modular monolith already enforces data ownership and interface-only communication, extraction is mechanical: the interface becomes an HTTP/gRPC client, in-process events become a message broker (Azure Service Bus, RabbitMQ), and the schema moves out with its module.
Events inside the monolith: practicing distributed patterns cheaply
The modular monolith lets you adopt the communication style of microservices before paying their tax. Instead of the Blogs module calling Notifications directly, it raises an event; a handler in the other module reacts:
public record PostPublished(long PostId, string Title, long AuthorId);
// Blogs module — publisher side
await mediator.Publish(new PostPublished(post.Id, post.Title, post.AuthorId), ct);
// Notifications module — subscriber side
public class PostPublishedHandler(INotificationService notifications)
: INotificationHandler<PostPublished>
{
public Task Handle(PostPublished e, CancellationToken ct) =>
notifications.FanOutToFollowersAsync(e.AuthorId, $"New post: {e.Title}", ct);
}
Today this is an in-process call with a transaction around both sides — simple and atomic. But the coupling shape is already message-shaped: Blogs doesn't know Notifications exists. When extraction day comes, mediator.Publish becomes a broker publish, the handler moves into its own service, and the outbox pattern replaces the shared transaction. You rehearsed the architecture without renting the stage.
The same applies to data: modules that already query each other through interfaces (never each other's tables) can have those interfaces re-implemented as HTTP clients in an afternoon. Every shortcut you don't take in the monolith is an extraction you don't dread later.
A worked example: when we would split this platform
Concrete beats abstract. The platform behind this blog runs Blogs, Identity, Training, and Tags modules in one container. Under what pressure would each actually leave?
- Identity: if a second product (say a separate hiring portal) needed the same accounts — shared identity across deployables is the classic first extraction, and its module boundary (own schema, interface-only access) makes it the easiest.
- Blogs' image/media handling: if uploads grew heavy, the media pipeline — not the whole module — would move to a worker service behind a queue. Extractions can be thinner than a module.
- Training's video sessions: different scaling profile (spiky, bandwidth-heavy) and a natural async boundary (enrollment events). A real 10× scale asymmetry — the legitimate trigger.
- Tags: never. It's small, synchronous, and consulted by everything; as a network service it would add a hop to every page for zero benefit.
Notice the pattern: each answer names a specific forcing pressure and a specific boundary. "We should do microservices" names neither — and that sentence, unaccompanied by a pressure, is the most expensive sentence in backend engineering.
If and when you do go distributed
The starter kit for surviving it:
- Async first. Prefer events (publish
OrderPlaced, let Notifications react) over synchronous call chains; queues absorb what timeouts amplify. - Outbox pattern for atomically saving state + publishing events — no dual-write bug.
- Timeouts, retries with jitter, circuit breakers (
Microsoft.Extensions.Http.Resilience) on every synchronous edge. - Contract discipline: consumer-driven tests or at minimum versioned, additive-only APIs.
- One gateway (YARP, Azure API Management) so clients see a coherent API while services shuffle behind it.
Notice every item is pure overhead relative to the monolith — necessary overhead once distributed, waste before then.
The roadmap position
Parts 1–8 quietly built the pre-microservices checklist: enforced internal boundaries (1–2), separated read models (3), stateless auth that any number of services can validate (4), caching layers (5), containerization (6), a deployment target that runs N services as easily as one (7), and pipelines (8). Whether this codebase stays a modular monolith forever or extracts its first service next quarter, nothing needs to be rebuilt — only re-wired.
Next
The moment work spans processes — or even before — "what is the system doing right now?" stops being answerable with a debugger. Part 10 closes the series with observability: OpenTelemetry traces, metrics, and structured logs, captured from our running API.
Comments (0)
No comments yet — be the first to share your thoughts.