Every engineer eventually hits the same wall: the code works, but the system doesn't. Requests time out under load. One team's deploy breaks another team's service. Nobody can explain why the database is the bottleneck at 2 AM. That's not a coding problem — that's a system design problem.
Let's walk through the architecture patterns that actually matter in 2026, with real examples instead of whiteboard theory.
Monolith vs Microservices: The Question Everyone Gets Wrong
The internet loves to frame this as a binary choice. It isn't. The real question is: how much independent scaling and deployment do you actually need right now?
A monolith isn't a legacy mistake — it's often the correct starting point.
// A well-structured monolith: one deployable, clear internal boundaries
// src/modules/orders/orderService.js
export async function createOrder(userId, items) {
const user = await userRepository.findById(userId);
const total = calculateTotal(items);
const order = await orderRepository.create({ userId, items, total });
await inventoryService.reserveStock(items);
await notificationService.sendOrderConfirmation(user.email, order);
return order;
}
Everything above lives in one codebase, one deploy pipeline, one database. That's not a weakness — it's simplicity you'll miss once you split it up.
Microservices earn their complexity when teams need to ship independently, or when one part of the system has wildly different scaling needs than the rest.
// order-service (independent deployable)
app.post("/orders", async (req, res) => {
const order = await createOrder(req.body);
// Fire-and-forget event instead of a direct call
await eventBus.publish("order.created", { orderId: order.id, userId: order.userId });
res.status(201).json(order);
});
// inventory-service (separate deployable, subscribes to the event)
eventBus.subscribe("order.created", async (event) => {
await reserveStock(event.orderId);
});
Notice the shift: instead of orderService directly calling inventoryService, they communicate through events. That decoupling is the entire point of microservices — and also the entire source of their complexity (retries, eventual consistency, distributed tracing).
Event-Driven Architecture: Decoupling as a Feature
Once you have more than a couple of services, direct HTTP calls between them create a web of tight coupling. Event-driven design flips that:
// A minimal event bus abstraction (backed by Kafka, SQS, or Redis Streams)
class EventBus {
async publish(topic, payload) {
await this.broker.send(topic, JSON.stringify(payload));
}
subscribe(topic, handler) {
this.broker.on(topic, async (message) => {
try {
await handler(JSON.parse(message));
} catch (err) {
await this.deadLetterQueue.push(topic, message, err);
}
});
}
}
The dead-letter queue matters more than people think — in a distributed system, failures are guaranteed, not exceptional. Design for them from day one, not after the first 3 AM page.
The Database Layer: Where Most Systems Actually Break
Most "system design" failures aren't about services — they're about data. A few patterns worth internalizing:
Read replicas for read-heavy workloads:
const writeDb = new Pool({ host: "primary.db.internal" });
const readDb = new Pool({ host: "replica.db.internal" });
async function getOrder(orderId) {
return readDb.query("SELECT * FROM orders WHERE id = $1", [orderId]);
}
async function createOrder(data) {
return writeDb.query("INSERT INTO orders (...) VALUES (...)", [data]);
}
Caching in front of the database:
async function getProduct(productId) {
const cached = await redis.get(`product:${productId}`);
if (cached) return JSON.parse(cached);
const product = await db.query("SELECT * FROM products WHERE id = $1", [productId]);
await redis.set(`product:${productId}`, JSON.stringify(product), "EX", 300);
return product;
}
Cache invalidation is the classic hard problem here — a 5-minute TTL is a blunt but honest tool until you need something smarter like write-through invalidation.
Choosing Patterns Without Overengineering
A rough guide that's saved me from a lot of premature complexity:
| Signal | Likely Fit |
|---|---|
| Single small team, unclear scaling needs | Monolith |
| Multiple teams, independent release cycles | Microservices |
| High write volume, async processing tolerable | Event-driven |
| Read-heavy, latency-sensitive | Caching + read replicas |
| Complex workflows spanning services | Orchestration (Step Functions, Temporal) |
If you're documenting these decisions across a growing team, a software hub is worth setting up early — architecture decision records get lost in Slack threads fast, and having one place where the "why" behind each pattern lives saves a lot of re-litigating old debates six months later.
A Practical Middle Ground: Modular Monolith
If microservices feel premature but a tangled monolith scares you, there's a well-tested middle ground: structure the monolith with hard internal boundaries, as if it were already split.
src/
modules/
orders/ # owns its own DB tables, no cross-module SQL
inventory/
notifications/
shared/
eventBus.js # internal pub/sub, same process
// modules/orders/index.js — the only public interface to this module
export { createOrder, getOrder, cancelOrder } from "./orderService.js";
// Nothing else is importable from outside this folder
This gets you most of the discipline of microservices — clear ownership, enforced boundaries — without the operational tax of distributed systems. When (and if) you actually need to split a module into its own service later, the boundary is already drawn.
Wrapping Up
Good system design isn't about picking the trendiest pattern — it's about matching the architecture to the actual constraints: team size, scaling needs, consistency requirements. Start simpler than you think you need to. The cost of splitting a monolith later is real but manageable. The cost of prematurely distributing a system you didn't need to distribute is much harder to undo.
Want a deeper breakdown of trade-offs between these patterns? Check out our *Serverless Architecture Guide** for how these ideas play out once you add serverless and edge compute into the mix.*
Top comments (0)