Modern Architecture: From Monoliths to Microservices

Part 5 of 5 in the System Design Series


In 2001, Amazon was a single application. One massive codebase, handling product listings, search, checkout, recommendations, inventory, shipping, all woven together. Every developer worked in the same repository. Every deployment touched everything. A change to the search feature could accidentally break the checkout flow.

Jeff Bezos eventually sent an email that changed the trajectory of software architecture. The mandate: every team must expose its data and functionality through service interfaces. Teams must communicate through those interfaces and nothing else. No exceptions.

That memo planted the seeds of what we now call microservices architecture, and it fundamentally changed how large-scale software is built.


The Monolith: A Good Start That Becomes a Problem

Almost every application starts as a monolith: a single codebase where all features live together. The authentication logic, the payment processing, the notification system, the user profiles, all in one place, sharing the same database, deployed as one unit.

For early-stage products, this is genuinely the right choice. It's simpler to build, simpler to debug, and simpler to deploy. When you're a small team moving fast, a monolith is your friend.

But as the system grows, more features, more developers, more users, cracks start to appear:

  • A bug in one module can take down the entire application
  • Deploying a small change requires redeploying everything
  • Scaling the whole application to handle load on just one feature is wasteful
  • Different parts of the system can't be built with different technologies
The system that was easy to build becomes painful to maintain.


Microservices: Break It Apart

Microservices architecture is the response to this pain. Instead of one large application, you decompose the system into smaller, independent services, each responsible for a single, clearly scoped function.

The payment service handles payments. The notification service handles emails and push notifications. The user service handles accounts and authentication. They each have their own codebase and their own database.

The benefits are significant:

Independent deployment. If you fix a bug in the payment service, you deploy only the payment service. Nothing else is touched. No risk of breaking notifications or search.

Independent scaling. During Black Friday, your checkout service gets hammered but your email service barely sees a spike. With microservices, you scale checkout independently, no need to spin up extra instances of services that don't need it.

Technology freedom. Your data science team can build the recommendation engine in Python. Your infrastructure team can write the file processing service in Go. Your main API can stay in Node.js. Each service picks the right tool for its job.

Fault isolation. If the recommendation service crashes, the rest of the application keeps running. Users just don't see personalised recommendations for a bit, not a white error screen.


The New Complexity: How Do Services Talk?

Breaking the application apart creates a coordination problem. Services need to communicate with each other constantly. How does that work?

The straightforward answer is direct API calls: the checkout service calls the notification service to send a confirmation email. Simple enough.

But what happens when traffic spikes and the notification service can't keep up? Every service calling it directly will pile up failed requests. The bottleneck in one place starts to cascade everywhere.

This is where message queues come in.

Instead of calling the notification service directly, the checkout service drops a message into a queue: "Send a confirmation email for order #4892." The notification service reads from the queue at its own pace and processes jobs as it can.

The two services are now decoupled. The checkout service doesn't wait for the email to be sent, it just confirms the order and moves on. If the notification service goes down temporarily, messages pile up in the queue and get processed once it recovers. Nothing is lost.

Popular message queue systems include RabbitMQ and Apache Kafka. This pattern, called asynchronous communication, is fundamental to building resilient distributed systems.


The API Gateway: One Front Door

With dozens of microservices running independently, exposing all of them directly to the outside world would be a mess. Every client, your mobile app, your web app, third-party integrations, would need to know the address of every service. Security would be a nightmare.

The solution is an API Gateway: a single entry point that sits in front of all your services and acts as the system's front door.

When a request comes in, the gateway figures out which service should handle it and routes the request there. From the outside, it looks like you're talking to one system. On the inside, the gateway is orchestrating calls to multiple services on your behalf.

But the API gateway does more than routing:

Rate limiting. If a single user, or a malicious bot, is sending thousands of requests per second, the gateway can throttle them before they ever reach your services. This prevents any one actor from overwhelming your system, and it's the first line of defence against denial-of-service attacks.

Authentication. Instead of every service independently verifying whether a user is logged in, the gateway can handle authentication centrally. If the request passes, it gets forwarded. If not, it's rejected before it goes anywhere.

Idempotency. Here's a subtle but critical problem: what happens if a user's "Place Order" request gets sent twice? Maybe their network was slow and they hit the button again, or their app automatically retried a failed request. Should your system charge them twice?

Idempotency is the design principle that prevents this. Each request is assigned a unique ID. If the same ID arrives twice, the system recognises it as a duplicate and ignores the second one, returning the same result as the first without processing it again.

This is why you can refresh a payment confirmation page without being charged a second time. It's not an accident. It was designed that way.


The Full Picture

Over five posts, we've traced a path from the most fundamental question, how do two computers talk?, to the architecture patterns powering the systems billions of people use every day.

Here's how it all connects:

A user's request arrives via HTTP/HTTPS, navigating through DNS and potentially a reverse proxy before hitting your API gateway. The gateway authenticates the request, applies rate limiting, and routes it to the right microservice. That service might read from a cached result, query a replicated database, or drop a job onto a message queue for async processing. Large files are served from blob storage via a CDN close to the user. For real-time updates, a WebSocket connection keeps the client in sync.

The concepts in this series aren't exotic. They're the vocabulary of modern backend engineering, patterns you'll encounter in job interviews, architecture discussions, and the systems you'll build throughout your career.

The next step is applying them. Pick one concept from this series, find it in a system you use every day, and think about why it's there.


Previous: Part 4 → Going Global: Distributed Systems and Real-Time Data


Thanks for reading this series. If you found it useful, share it with someone learning backend development.