🏗️

Software Architecture

Design systems that scale, survive, and evolve. From SOLID principles to microservices, CAP theorem to event-driven design.

Monolith vs Microservices vs Serverless

Your architecture choice is the most consequential technical decision you'll make. Get it wrong and you'll spend years paying the cost. Get it right and you can scale to millions of users.

🧱 Monolith

All code in a single deployable unit. The entire application — UI, business logic, data access — lives together. Every change requires redeploying the whole app.

  • Simple to develop
  • Easy to test end-to-end
  • No network latency between components
  • One deployment
  • Hard to scale individual parts
  • Slow deployments at scale
  • Tech lock-in
  • Team bottlenecks
✅ Best for: Early-stage products, small teams (<10 devs), internal tools

🔧 Microservices

Application split into small, independent services. Each service owns its data, has its own deployment pipeline, and communicates via APIs or events.

  • Independent scaling
  • Independent deployments
  • Different tech stacks per service
  • Team autonomy
  • Distributed systems complexity
  • Network failures, latency
  • Data consistency challenges
  • Needs DevOps maturity
✅ Best for: Large teams (10+ devs), high-scale products, domain-separated systems

⚡ Serverless

Functions deployed on-demand, no server management. Cloud (AWS Lambda, Google Cloud Functions) handles scaling automatically. Pay per execution.

  • Zero server management
  • Auto-scaling to zero
  • Pay per use (cheap for low traffic)
  • Fast to deploy
  • Cold starts (latency)
  • Vendor lock-in
  • Hard to debug locally
  • 15-min max execution time
✅ Best for: Event-driven workloads, APIs with unpredictable traffic, data pipelines
✅ Netflix's Architecture Evolution Netflix started as a monolith in 2007. After a major database corruption took them offline for 3 days in 2008, they spent 7 years migrating to 700+ microservices on AWS. Today they deploy 100+ times per day. Lesson: Start with a well-structured monolith, migrate to microservices when team size and scale demand it.

SOLID Principles

SOLID is a set of 5 design principles that make code maintainable, extensible, and testable. Introduced by Robert C. Martin ("Uncle Bob"), they're the foundation of clean object-oriented design.

S — Single Responsibility Principle

A class should have only one reason to change. If your UserService sends emails, generates reports, AND handles auth — that's 3 reasons to change.

// ❌ Bad: UserService doing too much
class UserService {
  createUser(data) { ... }
  sendWelcomeEmail(user) { ... }  // email responsibility
  generatePDFReport(user) { ... } // report responsibility
}

// ✅ Good: Separated concerns
class UserService { createUser(data) { ... } }
class EmailService { sendWelcome(user) { ... } }
class ReportService { generatePDF(user) { ... } }

O — Open/Closed Principle

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

// ❌ Bad: Adding new payment method requires editing existing code
function processPayment(type, amount) {
  if (type === 'card') { ... }
  else if (type === 'paypal') { ... }  // must modify this function each time
}

// ✅ Good: Extend by adding a new class
class CardPayment { process(amount) { ... } }
class PayPalPayment { process(amount) { ... } }
class CryptoPayment { process(amount) { ... } } // just add new class

function processPayment(provider, amount) {
  provider.process(amount); // no modification needed
}

D — Dependency Inversion Principle

High-level modules should not depend on low-level modules. Both should depend on abstractions (interfaces). This is the foundation of testable code.

// ❌ Bad: Hard dependency — can't test without real database
class UserService {
  constructor() {
    this.db = new PostgresDatabase(); // tightly coupled
  }
}

// ✅ Good: Inject the dependency — easy to swap for tests
class UserService {
  constructor(database) {  // inject any DB implementation
    this.db = database;
  }
}

// Production: new UserService(new PostgresDatabase())
// Tests:      new UserService(new InMemoryDatabase())

CAP Theorem

In a distributed system, you can only guarantee 2 of 3 properties: Consistency, Availability, and Partition Tolerance. Partition tolerance is always required in real networks, so you choose between CP and AP.

Consistency All nodes same data Availability Always responds Partition Network splits ok CP: MongoDB AP: Cassandra CA: PostgreSQL
TypePropertiesExamplesUse When
CPConsistent + Partition TolerantMongoDB, HBase, Redis (cluster)Financial transactions, inventory — correctness matters more than uptime
APAvailable + Partition TolerantCassandra, CouchDB, DynamoDBSocial feeds, analytics — eventual consistency acceptable
CAConsistent + AvailablePostgreSQL, MySQL (single node)Single-node systems — no distributed partition tolerance needed

API Design: REST vs GraphQL vs gRPC

Your API is a contract with consumers. Choose the right protocol based on your use case, team, and client types.

AspectRESTGraphQLgRPC
ProtocolHTTP/1.1 + JSONHTTP/1.1 + JSONHTTP/2 + Protobuf
FlexibilityFixed endpointsQuery exactly what you needStrongly typed contracts
PerformanceMediumMedium (N+1 risk)⚡ Very fast (binary)
Over/Under-fetchingCommon problemSolved by designDefined by proto schema
Browser support✅ Native✅ Native❌ Needs grpc-web
Best forPublic APIs, simple CRUDMobile apps, complex queriesInternal microservices
Used byTwitter, Stripe, GitHubGitHub v4, Shopify, FacebookGoogle, Netflix internal
// REST: Multiple requests for related data
GET /users/123            → { id, name, email }
GET /users/123/expenses   → [{ id, amount, category }]
GET /users/123/budget     → { monthly_limit, spent }

// GraphQL: Single request, get exactly what you need
query {
  user(id: "123") {
    name
    expenses(limit: 5) { amount category date }
    budget { monthly_limit spent }
  }
}
// Returns exactly the shape requested — no over-fetching
💡 12-Factor App The 12-Factor methodology (by Heroku engineers) defines best practices for cloud-native apps: store config in environment variables, treat backing services (DB, cache) as attached resources, export logs as event streams, and keep stateless processes. Follow these 12 factors and your app will be deployable anywhere.