thecodex.expert · The Codex Family of Knowledge
Software Engineering

System Architecture

Architecture is about the decisions that are hard to change later. Get them right early; the rest is just code.

SOLID principles Monolith · Microservices · Events Last verified:
Canonical Definition

System architecture defines the high-level structure of a software system — how it is divided into components, how those components communicate, and what trade-offs were made. Architecture decisions are hard to change: monolith vs microservices, sync vs async communication, SQL vs NoSQL. The SOLID principles guide class and module-level design. The CAP theorem constrains what a distributed system can guarantee simultaneously.

SOLID Principles

The SOLID principles, coined by Robert Martin, describe five properties of well-designed class-level code that make it easier to extend and maintain.

S — Single Responsibility Principle

A class should have one reason to change — one job. A User class shouldn't also handle email sending, CSV export, and database persistence. Each responsibility is a reason to change; mixing them makes classes fragile. Split into UserRepository, UserEmailService, UserExporter.

O — Open/Closed Principle

Classes should be open for extension, closed for modification. Add new behaviour by adding new code (implementing an interface, adding a subclass), not by editing existing code. Adding a new payment method shouldn't require editing the existing payment processing logic.

L — Liskov Substitution Principle

Subclasses should be substitutable for their parent class. If your code expects a Bird and you pass a Penguin (which can't fly), code that calls bird.fly() breaks. The parent's contract must be honoured by all subtypes.

I — Interface Segregation Principle

Clients shouldn't be forced to depend on interfaces they don't use. A fat interface with 20 methods forces every implementor to implement all 20, even if they only need 3. Split fat interfaces into smaller, focused ones.

D — Dependency Inversion Principle

High-level modules shouldn't depend on low-level modules — both should depend on abstractions. OrderService shouldn't import PostgresOrderRepository. It should depend on an OrderRepository interface; the concrete database implementation is injected. This makes swapping implementations (and testing with mocks) possible.

Monolith vs Microservices

This is the most over-debated architectural decision in software. The answer: start with a monolith. Move to microservices only when you have a specific problem the monolith can't solve.

Monolith ✓
  • Simple to develop, deploy, debug
  • No network latency between components
  • Transactions are easy (single database)
  • Easy to refactor across modules
  • Right for most teams and most products
Microservices — when warranted
  • Different services need different scaling
  • Independent deployment is required
  • Teams are large enough to own separate services
  • Different parts need different tech stacks
  • Adds: network complexity, distributed transactions, observability overhead
The monolith-first rule

Martin Fowler's "MonolithFirst" pattern: build a monolith first, understand your domain, then extract microservices where the seams are clear. Prematurely splitting into microservices before you understand the domain creates "distributed monoliths" — the worst of both worlds: the deployment complexity of microservices with the coupling of a monolith.

API Design: REST vs GraphQL vs gRPC

The right API style depends on your clients and use case, not fashion.

Aspect REST GraphQL gRPC
Best for Public APIs, simple CRUD, well-understood resources Complex data graphs, mobile clients with variable bandwidth, self-documenting APIs Internal service-to-service, high performance, streaming
Over/under-fetching Common — fixed response shapes Solved — clients request exactly what they need Fixed schema (Protocol Buffers)
Versioning /v1/users, /v2/users Schema evolution via deprecation Proto field numbering
Complexity Simple High — N+1 queries, schema design, resolver complexity Medium — requires proto tooling

Event-Driven Architecture

Instead of service A calling service B directly (synchronous coupling), A publishes an event to a message broker. B subscribes and reacts asynchronously. This decouples services — A doesn't need to know B exists. Multiple services can react to the same event independently.

Pythonevent_driven.py
# Event-driven: OrderService publishes events,
# other services react independently

# Without events (tight coupling):
class OrderService:
    def place_order(self, order):
        self.db.save(order)
        self.email_service.send_confirmation(order)  # coupled to email
        self.inventory.reserve(order.items)           # coupled to inventory
        self.analytics.track("order_placed", order)   # coupled to analytics
        # adding a new action requires editing this class

# With events (loose coupling):
class OrderService:
    def place_order(self, order):
        self.db.save(order)
        self.event_bus.publish("order.placed", {
            "order_id": order.id,
            "user_id": order.user_id,
            "items": order.items,
            "total": order.total,
        })
        # OrderService is done — doesn't know who listens

# Each subscriber is independent:
class EmailSubscriber:
    def on_order_placed(self, event): ...

class InventorySubscriber:
    def on_order_placed(self, event): ...

class AnalyticsSubscriber:
    def on_order_placed(self, event): ...

# Adding a new subscriber requires no changes to OrderService

Trade-offs: Event-driven systems are harder to trace and debug (distributed transactions are complex), but they scale better and allow teams to work independently. Use message brokers (Kafka, RabbitMQ, AWS SQS) for production event systems.

CAP Theorem

Eric Brewer's CAP theorem states that a distributed data store can provide at most two of three guarantees simultaneously:

⚖️
Consistency

All nodes see the same data at the same time. Every read returns the most recent write.

⬆️
Availability

Every request receives a response — not necessarily the most recent data.

🔗
Partition tolerance

The system works when network messages between nodes are lost or delayed.

Since network partitions are inevitable in distributed systems, real systems must choose between CP (consistent but may be unavailable during partition — e.g. HBase, Zookeeper) or AP (available but may return stale data — e.g. Cassandra, DynamoDB, CouchDB). Most SQL databases are CA — not partition tolerant, which is why they're hard to distribute.

The practical implication: for most web applications, eventual consistency (AP) is acceptable. Your Twitter timeline doesn't need to be perfectly up-to-date to millisecond precision. Your bank balance does.

Architecture pitfalls
Premature optimisation at the architecture level. Building a complex event-driven microservices architecture for an app with 100 users. Start simple, add complexity only when you have the specific problem that complexity solves.
"Let's use [trending technology]" as an architecture driver. Architecture should be driven by requirements: expected load, team size, deployment constraints, data consistency needs. Not by what appeared on Hacker News last week.
Ignoring the data layer. Most architectural bottlenecks are in the database. Horizontal scaling of stateless services is easy; scaling data is hard. Design the data model early and get it right — schemas are the hardest thing to migrate.

Sources

1
Martin, R. C. (2017). Clean Architecture: A Craftsman's Guide to Software Structure and Design. Prentice Hall. — SOLID and architectural principles.
2
Fowler, M. martinfowler.com. — MonolithFirst, microservices, event-driven architecture patterns.
3
Brewer, E. A. (2000). "Towards Robust Distributed Systems." PODC Keynote. — CAP theorem original presentation.
4
Newman, S. (2021). Building Microservices (2nd ed.). O'Reilly. — When and how to decompose services.
Source confidence: High Last verified: Primary source: Martin (2017) · Fowler martinfowler.com