The Monolith
A monolith deploys the entire application as a single unit. All modules share the same process, memory, and database. Despite its reputation, a well-structured monolith is often the right starting point.
Strengths
Simple to develop, test, and deploy. A single codebase means easy debugging and straightforward local development. Database transactions are trivial since everything shares one database -- no distributed consistency headaches.
Weaknesses
As the codebase grows, tight coupling between modules makes changes risky. Scaling is all-or-nothing: you scale the entire application even if only one module is under load. Deployment of a one-line fix requires redeploying everything. Team autonomy suffers as everyone works in the same codebase.
Modular Monolith
The best of both worlds. The application is a single deployable unit, but internally it is organized into well-defined modules with explicit boundaries and APIs. Each module owns its data and communicates with others through defined interfaces. This gives you the simplicity of a monolith with the structural discipline that makes a future migration to microservices feasible.
Microservices Architecture
Each service owns its own database, deployment pipeline, and is typically maintained by a small, autonomous team. Services communicate over the network, introducing new trade-offs around consistency, latency, and operational complexity.
Communication Styles
| Aspect | Synchronous (HTTP / gRPC) | Asynchronous (Messaging) |
|---|---|---|
| Coupling | Temporal -- caller waits for response | Loose -- fire and forget or eventual reply |
| Latency | Direct, but cascading failures possible | Higher perceived latency, but resilient |
| Use case | Queries, real-time reads | Commands, event propagation, workflows |
| Tooling | REST, gRPC, GraphQL | Kafka, RabbitMQ, SQS, NATS |
Saga Pattern for Distributed Consistency
Without a shared database, you cannot use ACID transactions across services. The saga pattern coordinates a sequence of local transactions, where each service performs its step and publishes an event. If any step fails, compensating transactions undo the previous steps.
Event-Driven Architecture
Systems communicate by producing and consuming events rather than making direct calls. An event represents a fact -- something that happened -- and is immutable once published.
Types of Events
Business-meaningful occurrences: OrderPlaced, PaymentReceived. Published by aggregates after state changes.
Cross-boundary notifications between services. Typically a slimmer payload designed for external consumers.
Lightweight signals that something changed. Consumers must call back to get full details if needed.
Events carry enough data so consumers never need to call back. Reduces coupling but increases event size.
Benefits
Loose coupling between producer and consumer -- the producer does not know or care who consumes the event. Natural audit trail since events are an immutable log of what happened. Enables temporal decoupling: services can process events at their own pace. Scales well because consumers can be added without modifying the producer.
Challenges
Eventual consistency is the default -- the system may be temporarily inconsistent. Debugging is harder because there is no single request path to trace. Event ordering, idempotency, and exactly-once processing are non-trivial. Schema evolution of events requires careful versioning.
Hexagonal Architecture & Domain-Driven Design
Hexagonal (Ports & Adapters)
The core idea: your business logic sits at the center and knows nothing about the outside world. It defines ports (interfaces) for what it needs. Adapters implement those ports to connect to databases, APIs, message brokers, or UIs. Dependencies always point inward.
Bounded Contexts
A bounded context is a boundary within which a particular domain model applies. The same word (e.g., "Account") can mean different things in different contexts (banking vs. user management). Each context has its own ubiquitous language -- the shared vocabulary between developers and domain experts within that boundary.
DDD Building Blocks
| Concept | Definition | Example |
|---|---|---|
| Aggregate | Cluster of entities treated as a single unit for data changes. Has a root entity. | Order (root) + OrderLine items |
| Entity | Object with a unique identity that persists over time. | User, Order |
| Value Object | Immutable object defined by its attributes, not identity. Two VOs with same values are equal. | Money(100, "USD"), Address |
| Domain Event | Record of something meaningful that happened in the domain. | OrderShipped, PaymentFailed |
| Repository | Abstraction for persisting and retrieving aggregates. | OrderRepository.findById() |
Money(10, "USD") objects are the same). Aggregates enforce invariants -- all changes go through the aggregate root to maintain consistency.
SOLID Principles
Five principles for writing maintainable, flexible object-oriented code. They reduce coupling, increase cohesion, and make code easier to change.
A class should have only one reason to change. Separate concerns into different classes -- don't mix validation, persistence, and notification in one place.
Open for extension, closed for modification. Add new behavior by adding new code (new classes, new implementations), not by changing existing code.
Subtypes must be substitutable for their base types without breaking behavior. If Square extends Rectangle, it must honor Rectangle's contract.
Clients should not be forced to depend on interfaces they don't use. Prefer many small, focused interfaces over one large one.
High-level modules should not depend on low-level modules. Both should depend on abstractions. This is the principle behind hexagonal architecture's ports.
// Dependency Inversion in practice
// High-level module depends on abstraction, not concrete DB
interface OrderRepository {
save(order: Order): void;
findById(id: string): Order;
}
class OrderService {
constructor(private repo: OrderRepository) {} // depends on interface
placeOrder(order: Order) {
// business logic...
this.repo.save(order); // doesn't know if it's Postgres, Mongo, or in-memory
}
}
// Low-level module implements the abstraction
class PostgresOrderRepository implements OrderRepository {
save(order: Order) { /* SQL insert */ }
findById(id: string) { /* SQL select */ }
}
Design Patterns That Matter
Not all 23 GoF patterns are equally useful. Here are the ones you will actually reach for in modern backend and distributed systems.
Creational
Encapsulate object creation. The caller asks for an object without knowing the concrete class. Useful when creation logic is complex or varies by context.
Construct complex objects step by step. Avoids constructors with many parameters. Especially useful for test data and configuration objects.
Behavioral
Define a family of algorithms, encapsulate each one, and make them interchangeable. The client picks the strategy at runtime (e.g., different pricing rules, different sorting algorithms).
When one object changes state, all dependents are notified. The foundation of event-driven programming. Used in UI frameworks, event buses, and pub/sub systems.
Structural
Wrap an object to add behavior without modifying it. Stack decorators for composition: LoggingRepo(CachingRepo(PostgresRepo)).
A surrogate that controls access to another object. Used for lazy loading, access control, logging, and remote communication (RPC stubs).
Distributed System Patterns
Stop calling a failing service after repeated failures. After a timeout, allow a trial request. Prevents cascade failures across services. States: Closed (normal) → Open (failing) → Half-Open (testing).
Coordinate distributed transactions via a sequence of local transactions with compensating actions. Choreography (event-driven) or orchestration (central coordinator).
Write events to a local DB table atomically with business data. A relay process publishes them to the message broker. Solves the dual-write reliability problem.