two ways to structure a codebase

Picture a food-delivery app, PlateRoute. Early on, it has one team and one job: let a customer order food and get it delivered. Every feature — the menu browser, the cart, payments, driver dispatch — lives in a single codebase, deployed as one unit. That's a monolith.
As PlateRoute grows to dozens of engineers and millions of orders, that single codebase starts to strain. The alternative is a microservices architecture: split the same functionality into independently deployable services — a Menu Service, a Cart Service, a Payment Service, a Dispatch Service — each with its own codebase, its own database, and its own deployment pipeline.

why monoliths become painful at scale

ProblemWhy it happens
Single technology stackThe whole app is one language/runtime. Adding a component that's a much better fit in a different language (e.g. a Python ML model for recommended restaurants) means bolting on a separate service anyway or awkwardly forcing it into the existing stack.
Shared blast radiusA memory leak in the driver-tracking module can take down the checkout flow, because they're the same process.
All-or-nothing deploysEvery deploy ships the entire codebase, even for a one-line change to the menu page — slower builds, slower rollbacks, more coordination between teams.
Can't scale parts independentlyDuring a lunch rush, order volume spikes but account-settings traffic doesn't. A monolith scales as one unit, so idle code gets scaled along with the hot path.

what microservices trade for that

Splitting PlateRoute into services fixes those problems, but it isn't free. Four costs show up almost immediately:
CostWhat it looks like
LatencyA function call within a monolith is microseconds; a call to another service over the network is milliseconds, and can fail in ways a function call can't.
Distributed debuggingA single failed order now requires correlating logs across the Cart, Payment, and Dispatch services instead of reading one stack trace — this is why centralized logging becomes mandatory, not optional.
Service discovery & configEvery service needs to know where every other service currently lives, and that changes as instances scale up and down.
Operational overheadHealth checks, retries, access control, and inter-service authentication all need their own infrastructure once there are dozens of services instead of one.
None of this is free, and a two-person startup adopting a 30-microservice architecture on day one is usually solving a problem it doesn't have yet.

how to actually decide

A rough, practical rule: start with a monolith, and split out a service only when a specific, felt pain justifies the operational cost — a component that needs independent scaling, a team that needs to deploy independently of everyone else, or a piece of logic that genuinely benefits from a different language or database.
If the whole system serves a few hundred requests per second and is maintained by a handful of engineers, the network latency and multi-service debugging cost of microservices is rarely worth paying. If a system has hundreds of engineers across many teams, a monolith's shared-deploy bottleneck usually becomes the bigger cost. Most real systems, including PlateRoute at scale, end up somewhere in between — a handful of services split along genuine seams (payments, search, notifications) with a larger core that stays together until a specific reason to split it shows up.

related topics

reference