Design systems that scale, survive, and evolve. From SOLID principles to microservices, CAP theorem to event-driven design.
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.
All code in a single deployable unit. The entire application — UI, business logic, data access — lives together. Every change requires redeploying the whole app.
Application split into small, independent services. Each service owns its data, has its own deployment pipeline, and communicates via APIs or events.
Functions deployed on-demand, no server management. Cloud (AWS Lambda, Google Cloud Functions) handles scaling automatically. Pay per execution.
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.
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) { ... } }
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 }
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())
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.
| Type | Properties | Examples | Use When |
|---|---|---|---|
| CP | Consistent + Partition Tolerant | MongoDB, HBase, Redis (cluster) | Financial transactions, inventory — correctness matters more than uptime |
| AP | Available + Partition Tolerant | Cassandra, CouchDB, DynamoDB | Social feeds, analytics — eventual consistency acceptable |
| CA | Consistent + Available | PostgreSQL, MySQL (single node) | Single-node systems — no distributed partition tolerance needed |
Your API is a contract with consumers. Choose the right protocol based on your use case, team, and client types.
| Aspect | REST | GraphQL | gRPC |
|---|---|---|---|
| Protocol | HTTP/1.1 + JSON | HTTP/1.1 + JSON | HTTP/2 + Protobuf |
| Flexibility | Fixed endpoints | Query exactly what you need | Strongly typed contracts |
| Performance | Medium | Medium (N+1 risk) | ⚡ Very fast (binary) |
| Over/Under-fetching | Common problem | Solved by design | Defined by proto schema |
| Browser support | ✅ Native | ✅ Native | ❌ Needs grpc-web |
| Best for | Public APIs, simple CRUD | Mobile apps, complex queries | Internal microservices |
| Used by | Twitter, Stripe, GitHub | GitHub v4, Shopify, Facebook | Google, 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