Architecture

Software Architecture & Design

From monoliths to microservices, event-driven systems to clean architecture. The patterns, principles, and trade-offs that shape how we build software at scale.

01 / Monolithic Architecture

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.

Monolith vs Modular Monolith
UI
+
Business Logic
+
Data Access
=
Single Deployment

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.

Practical Advice
Start with a modular monolith. Extract microservices only when you have a clear, proven need -- such as independent scaling or different deployment cadences for specific modules.
02 / Microservices

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

AspectSynchronous (HTTP / gRPC)Asynchronous (Messaging)
CouplingTemporal -- caller waits for responseLoose -- fire and forget or eventual reply
LatencyDirect, but cascading failures possibleHigher perceived latency, but resilient
Use caseQueries, real-time readsCommands, event propagation, workflows
ToolingREST, gRPC, GraphQLKafka, 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.

Choreography-based Saga
Order Created
Payment Charged
Inventory Reserved
Order Confirmed
If any step fails → compensating transactions roll back previous steps
Watch Out
Microservices trade code complexity for operational complexity. You now need service discovery, distributed tracing, circuit breakers, and a solid CI/CD pipeline for each service. Don't adopt microservices to solve a team or process problem.
03 / Event-Driven Architecture

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

Domain Events

Business-meaningful occurrences: OrderPlaced, PaymentReceived. Published by aggregates after state changes.

Integration Events

Cross-boundary notifications between services. Typically a slimmer payload designed for external consumers.

Event Notifications

Lightweight signals that something changed. Consumers must call back to get full details if needed.

Event-Carried State Transfer

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.

Outbox Pattern
To reliably publish events, write the event to an "outbox" table in the same database transaction as the state change. A separate process polls the outbox and publishes events to the broker. This avoids the dual-write problem where the DB write succeeds but the event publish fails (or vice versa).
04 / Clean Architecture & DDD

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.

Dependency Direction
HTTP Adapter
Domain Core
DB Adapter
CLI Adapter
Ports (Interfaces)
Queue Adapter

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

ConceptDefinitionExample
AggregateCluster of entities treated as a single unit for data changes. Has a root entity.Order (root) + OrderLine items
EntityObject with a unique identity that persists over time.User, Order
Value ObjectImmutable object defined by its attributes, not identity. Two VOs with same values are equal.Money(100, "USD"), Address
Domain EventRecord of something meaningful that happened in the domain.OrderShipped, PaymentFailed
RepositoryAbstraction for persisting and retrieving aggregates.OrderRepository.findById()
Key Insight
Entities have identity (you track them by ID). Value objects have equality by value (two Money(10, "USD") objects are the same). Aggregates enforce invariants -- all changes go through the aggregate root to maintain consistency.
05 / SOLID Principles

SOLID Principles

Five principles for writing maintainable, flexible object-oriented code. They reduce coupling, increase cohesion, and make code easier to change.

S - Single Responsibility

A class should have only one reason to change. Separate concerns into different classes -- don't mix validation, persistence, and notification in one place.

O - Open/Closed

Open for extension, closed for modification. Add new behavior by adding new code (new classes, new implementations), not by changing existing code.

L - Liskov Substitution

Subtypes must be substitutable for their base types without breaking behavior. If Square extends Rectangle, it must honor Rectangle's contract.

I - Interface Segregation

Clients should not be forced to depend on interfaces they don't use. Prefer many small, focused interfaces over one large one.

D - Dependency Inversion

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 */ }
}
06 / Essential Patterns

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

Factory

Encapsulate object creation. The caller asks for an object without knowing the concrete class. Useful when creation logic is complex or varies by context.

Builder

Construct complex objects step by step. Avoids constructors with many parameters. Especially useful for test data and configuration objects.

Behavioral

Strategy

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).

Observer

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

Decorator

Wrap an object to add behavior without modifying it. Stack decorators for composition: LoggingRepo(CachingRepo(PostgresRepo)).

Proxy

A surrogate that controls access to another object. Used for lazy loading, access control, logging, and remote communication (RPC stubs).

Distributed System Patterns

Circuit Breaker

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).

Saga

Coordinate distributed transactions via a sequence of local transactions with compensating actions. Choreography (event-driven) or orchestration (central coordinator).

Outbox

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.

Pattern Selection
Patterns are tools, not goals. Apply them when you recognize the problem they solve. Over-engineering with patterns you don't need yet is worse than having no pattern at all.

Test Yourself

Score: 0 / 10
Question 01
What is the main advantage of a modular monolith over a traditional monolith?
A modular monolith keeps the simplicity of a single deployment but introduces explicit boundaries and APIs between modules. It does not scale modules independently or eliminate the shared database -- those are microservice traits.
Question 02
In the saga pattern, what happens when a step in the transaction sequence fails?
Sagas do not use atomic distributed rollbacks or two-phase commits. Instead, each service publishes a compensating action (e.g., refund payment, release inventory) to undo what the previous steps accomplished.
Question 03
Which communication style introduces temporal coupling between microservices?
Synchronous calls require both the caller and the callee to be available at the same time -- this is temporal coupling. Asynchronous messaging (Kafka, queues, pub/sub) decouples services in time.
Question 04
What problem does the outbox pattern solve?
The outbox pattern writes events to a database table in the same transaction as the business data change. A separate relay process reads from the outbox and publishes to the broker. This avoids the scenario where the DB write succeeds but the event publish fails (or vice versa).
Question 05
In hexagonal architecture, what is a "port"?
In hexagonal architecture, ports are interfaces (abstractions) defined by the domain. Adapters are the concrete implementations that plug into these ports to connect the domain to external systems like databases, APIs, or UIs.
Question 06
What distinguishes a value object from an entity in DDD?
Entities have a unique identifier that persists over time -- two entities with the same attributes but different IDs are different. Value objects have no identity; two value objects with the same attributes are considered equal (e.g., Money(10, "USD")).
Question 07
Which SOLID principle does hexagonal architecture directly implement?
Hexagonal architecture is a direct application of the Dependency Inversion Principle: the domain core defines interfaces (ports), and infrastructure (adapters) depends on those abstractions. High-level policy does not depend on low-level details.
Question 08
What are the three states of a circuit breaker?
A circuit breaker starts Closed (requests flow normally). After repeated failures, it transitions to Open (requests are immediately rejected). After a timeout, it moves to Half-Open, allowing a trial request to test if the downstream service has recovered.
Question 09
In DDD, what is a bounded context?
A bounded context defines the boundary where a particular domain model is valid. The same term (e.g., "Customer") can have different meanings in different bounded contexts. Each context maintains its own ubiquitous language shared between developers and domain experts.
Question 10
Which pattern allows adding behavior to an object without modifying its class, by wrapping it?
The Decorator pattern wraps an object with another object that has the same interface, adding behavior before or after delegating to the wrapped object. Example: LoggingRepo(CachingRepo(PostgresRepo)) adds logging and caching without modifying PostgresRepo.