Monoliths

What is a monolith?

You have probably already built one. A single Next.js app is a monolith: one repository, one build, and every push deploys the whole site. Most frontends start exactly like this.

A monolith is one codebase with one build and one deploy process.

  1. It is the simplest architecture, with low operational overhead and a simple mental model.
  2. Styles, routing, tests, state, components, pages, and dependencies all live in one repo, and any part can depend on any other part.
monolith/ONE REPO
ComponentsPagesStylesRoutingStateAPI layerAuthAssetsi18nTestsConfigDependencies
all coupled — they share state, build, and version
PUSHgit pushone commit
BUILDCI / CDts · lint · prettier · test · bundle
RELEASEDeploythe entire app

A monolith is not a bad word

My take on starting with a monolith is inspired by Martin Fowler's MonolithFirst.

A monolith is not a bad word. People often use it as an insult, but it just means a system that is built and deployed as one unit. Nothing stops a monolith from having a clean internal structure. The real problem is the messy version of it, often called the Big Ball of Mud: one giant codebase with no clear boundaries, where any file can import any other file, and over time everything depends on everything.

The modular monolith

What most teams actually want is a modular monolith. It is still one application with one deploy, but the code inside is split into parts with clear rules about what can import what. Nx's dependency rules are a good example of this style. Feature code holds the logic for one part of the app and is loaded only when the user visits that part. UI libraries hold only presentational components. Data-access libraries hold the API calls and state logic. Utility libraries sit at the bottom and depend on nothing else. This is more than a tidy folder structure. The rules stop parts of the app from quietly depending on each other, so one large codebase still feels small to work in.

Here is what that structure looks like inside one app:

my-app/ONE APP · CLEAR BOUNDARIES
my-app/
src/
features/
checkout/
search/
ui/
button/
modal/
data-access/
cart/
products/
utils/
formatting/
{}package.json
TStsconfig.json
imports flow one way: features use ui, data-access, and utils; utils imports nothing

When a monolith is the right default

A monolith is usually the right default when the product is young, when you are not yet sure where the boundaries between features should be, or when the team is small enough that everyone can talk to each other directly. Fowler's Monolith First argument is simple: early on, you need speed and fast feedback, and drawing the right boundaries up front is hard even for experienced teams. If you split too early, you will probably split in the wrong places. His writing on Conway's Law adds one more point: a team of ten or twenty people can build a monolith without problems. Splitting the system only starts to matter when the team grows so large that people can no longer coordinate directly.

If one team can still understand the whole product, splitting the frontend into separately built and deployed pieces mostly adds overhead. Every split adds a cost: more contracts between the pieces, more integration points that can break, more build pipelines to maintain, and more rules about who owns what. Fowler calls this cost the microservice premium, and the frontend version of it is just as real. Sometimes the cost is worth paying. For a small team, it usually is not.

Remember
  1. Almost all successful microservice stories started with a monolith that got too big and was broken up.
  2. Almost every system built as microservices from scratch ended up in big trouble. So don't over-engineer, and start with a monolith.

Problems with monoliths

  1. Multiple teams edit the same files, get in each other's work, and can get frequently blocked by merge conflicts.
  2. A one-line change still rebuilds, re-tests, and redeploys the entire application. As the codebase and the test suite grow, shipping anything to production can take 30 to 40 minutes.
  3. A bug in navigation can break authentication. Basically, every change is a risk to the whole product.
  4. Upgrading any dependency, like React or a chart library, means upgrading everything at once across the whole codebase. So no one does, and the code stays on old versions.

These problems have two ways out, and they are different tools for different pains. You can keep one deploy but organize the code and the tooling better, which is what a monorepo gives you. Or you can split the deploy itself into independently shipped pieces, which is what microfrontends are. The next two articles cover each in turn.