SYSTEM DESIGN AND DESIGN PATTERNS

5
(1)

SYSTEM DESIGN

System design is about architecting large-scale software systems. It’s how you build applications that handle millions of users, massive data, and stay reliable.

Core Principles

Scalability – Handle growing load (users, data, traffic) Reliability – System works correctly even when things fail Availability – System is operational and accessible Maintainability – Easy to update, debug, and extend Performance – Fast response times, efficient resource use


Scalability Strategies

Vertical Scaling (Scale Up)

Add more power to existing machines (CPU, RAM, disk)

Pros: Simple, no code changes Cons: Hardware limits, expensive, single point of failure

Horizontal Scaling (Scale Out)

Add more machines to distribute load

Pros: Nearly unlimited, cost-effective with commodity hardware Cons: Complexity, need load balancing, data consistency challenges


Key System Design Concepts

1. Load Balancing

Distributes incoming requests across multiple servers

Algorithms:

  • Round Robin: Request 1 → Server A, Request 2 → Server B, Request 3 → Server C
  • Least Connections: Send to server with fewest active connections
  • IP Hash: Same client always goes to same server (session affinity)
  • Weighted: Send more traffic to more powerful servers

Types:

  • Layer 4 (Transport): Routes based on IP/port (fast, simple)
  • Layer 7 (Application): Routes based on content/cookies (flexible, slower)

Example Architecture:

                    Load Balancer
                         |
        +----------------+----------------+
        |                |                |
    Server 1         Server 2         Server 3

2. Caching

Store frequently accessed data in fast storage

Cache Levels:

  • Client-side: Browser cache
  • CDN: Cache static assets (images, CSS, JS) near users
  • Application: Redis, Memcached
  • Database: Query result cache

Cache Strategies:

Cache-Aside (Lazy Loading):

1. App checks cache
2. If miss, fetch from database
3. Store in cache
4. Return data

Write-Through:

1. Write to cache
2. Cache writes to database
3. Return success

Write-Behind:

1. Write to cache
2. Return success immediately
3. Cache writes to database asynchronously

Cache Eviction Policies:

  • LRU (Least Recently Used): Remove least accessed items
  • LFU (Least Frequently Used): Remove items accessed least often
  • FIFO: Remove oldest items first
  • TTL (Time To Live): Expire after set time

3. Database Strategies

Replication:

  • Master-Slave: Master handles writes, slaves handle reads
  • Master-Master: Multiple masters, both read and write

Partitioning/Sharding: Split data across multiple databases

Horizontal Sharding:

Users 1-1M     → Database 1
Users 1M-2M    → Database 2
Users 2M-3M    → Database 3

Sharding Strategies:

  • Range-based: User IDs 0-1000, 1001-2000, etc.
  • Hash-based: hash(user_id) % num_shards
  • Geographic: US users, EU users, Asia users
  • Directory-based: Lookup table maps keys to shards

SQL vs NoSQL:

SQL (Relational):

  • Structured data with relationships
  • ACID transactions
  • Complex queries (JOINs)
  • Examples: PostgreSQL, MySQL

NoSQL:

  • Document: MongoDB (JSON documents)
  • Key-Value: Redis, DynamoDB (fast lookups)
  • Column-family: Cassandra (wide tables, high write throughput)
  • Graph: Neo4j (relationships are first-class)

Use SQL by default, NoSQL when you need specific benefits (scalability, flexibility, speed)

4. CAP Theorem

You can only have 2 of 3:

Consistency: All nodes see same data at same time Availability: Every request gets a response Partition Tolerance: System works despite network failures

Real-world choices:

  • CP: MongoDB, HBase (consistent, may be unavailable during partitions)
  • AP: Cassandra, DynamoDB (available, eventual consistency)
  • CA: Traditional RDBMS (but networks always partition, so this is theoretical)

5. Message Queues

Asynchronous communication between services

Benefits:

  • Decouple services
  • Handle traffic spikes
  • Retry failed operations
  • Process tasks in background

Popular Options:

  • RabbitMQ
  • Apache Kafka (high-throughput streaming)
  • AWS SQS
  • Redis (simple pub/sub)

Example:

User uploads video → Queue → Worker processes video → Queue → Worker sends notification

6. Microservices vs Monolith

Monolith:

  • Single codebase, deployed as one unit
  • Simple to develop initially
  • Hard to scale, deploy, maintain at scale

Microservices:

  • Multiple small services, each with specific responsibility
  • Independent deployment and scaling
  • More complex (need service discovery, API gateway, monitoring)

When to use microservices:

  • Large team
  • Need to scale parts independently
  • Different tech stacks make sense
  • Want faster deployment cycles

7. API Design

REST:

GET    /users          - List users
GET    /users/123      - Get user
POST   /users          - Create user
PUT    /users/123      - Update user
DELETE /users/123      - Delete user

GraphQL:

query {
  user(id: 123) {
    name
    email
    posts {
      title
    }
  }
}

Client requests exactly what it needs, single endpoint

gRPC:

  • Binary protocol (faster than JSON)
  • Strong typing with Protocol Buffers
  • Bi-directional streaming

8. Rate Limiting

Prevent abuse and ensure fair usage

Algorithms:

Token Bucket:

  • Bucket holds tokens
  • Request consumes token
  • Tokens refill at fixed rate
  • If no tokens, request denied

Leaky Bucket:

  • Requests enter queue
  • Process at fixed rate
  • Overflow requests rejected

Fixed Window:

  • 100 requests per minute
  • Counter resets every minute
  • Problem: Burst at window boundaries

Sliding Window:

  • Smooths out fixed window issues
  • Considers requests in rolling time period

System Design Example: URL Shortener (like bit.ly)

Requirements:

  • Shorten URLs
  • Redirect to original URLs
  • Handle 100M URLs, 1000 requests/second
  • URLs don’t expire

Design:

1. API:

POST /shorten
Body: { "url": "https://example.com/very/long/url" }
Response: { "short_url": "abc123" }

GET /{short_code}
Redirects to original URL

2. Generate Short Codes:

  • Base62 encoding (a-z, A-Z, 0-9) = 62 characters
  • 7 characters = 62^7 = 3.5 trillion possibilities
  • Use auto-increment ID, convert to base62

3. Database Schema:

URLs (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  original_url TEXT NOT NULL,
  short_code VARCHAR(10) UNIQUE NOT NULL,
  created_at TIMESTAMP,
  INDEX(short_code)
)

4. Architecture:

User → Load Balancer → App Servers → Cache (Redis) → Database
                                           ↓
                                       Metrics

5. Optimization:

  • Cache popular URLs in Redis (80/20 rule)
  • Use CDN for static assets
  • Database read replicas for redirects
  • Rate limiting per IP

6. Scalability:

  • Shard database by short_code range
  • Multiple app servers behind load balancer
  • Pre-generate and cache short codes

DESIGN PATTERNS

Design patterns are reusable solutions to common programming problems. They’re templates, not finished code.

Categories

  1. Creational – Object creation
  2. Structural – Object composition
  3. Behavioral – Object interaction

Creational Patterns

1. Singleton

Ensure only one instance of a class exists

Use case: Database connection, logger, configuration manager

class Database:
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance.connection = create_connection()
        return cls._instance

# Usage
db1 = Database()
db2 = Database()
# db1 and db2 are the same instance

Pros: Single point of control, lazy initialization Cons: Global state (testing issues), thread-safety concerns

2. Factory

Create objects without specifying exact class

Use case: Creating different types of objects based on input

class Dog:
    def speak(self):
        return "Woof!"

class Cat:
    def speak(self):
        return "Meow!"

class AnimalFactory:
    def create_animal(self, animal_type):
        if animal_type == "dog":
            return Dog()
        elif animal_type == "cat":
            return Cat()

# Usage
factory = AnimalFactory()
pet = factory.create_animal("dog")
print(pet.speak())  # "Woof!"

3. Builder

Construct complex objects step by step

Use case: Creating objects with many optional parameters

class Pizza:
    def __init__(self):
        self.size = None
        self.cheese = False
        self.pepperoni = False
        self.mushrooms = False

class PizzaBuilder:
    def __init__(self):
        self.pizza = Pizza()
    
    def set_size(self, size):
        self.pizza.size = size
        return self
    
    def add_cheese(self):
        self.pizza.cheese = True
        return self
    
    def add_pepperoni(self):
        self.pizza.pepperoni = True
        return self
    
    def build(self):
        return self.pizza

# Usage
pizza = PizzaBuilder()\
    .set_size("large")\
    .add_cheese()\
    .add_pepperoni()\
    .build()

Structural Patterns

4. Adapter

Make incompatible interfaces work together

Use case: Integrating third-party libraries, legacy code

# Old interface
class OldPaymentSystem:
    def make_payment(self, amount):
        print(f"Old system: Processing ${amount}")

# New interface expected by our app
class PaymentProcessor:
    def process(self, amount):
        pass

# Adapter
class PaymentAdapter(PaymentProcessor):
    def __init__(self, old_system):
        self.old_system = old_system
    
    def process(self, amount):
        self.old_system.make_payment(amount)

# Usage
old_system = OldPaymentSystem()
adapter = PaymentAdapter(old_system)
adapter.process(100)  # Works with new interface!

5. Decorator

Add behavior to objects dynamically

Use case: Adding features without modifying original class

class Coffee:
    def cost(self):
        return 5

class MilkDecorator:
    def __init__(self, coffee):
        self.coffee = coffee
    
    def cost(self):
        return self.coffee.cost() + 2

class SugarDecorator:
    def __init__(self, coffee):
        self.coffee = coffee
    
    def cost(self):
        return self.coffee.cost() + 1

# Usage
coffee = Coffee()
coffee_with_milk = MilkDecorator(coffee)
coffee_with_milk_and_sugar = SugarDecorator(coffee_with_milk)
print(coffee_with_milk_and_sugar.cost())  # 8

6. Facade

Simplified interface to complex subsystem

Use case: Hide complexity, provide simple API

# Complex subsystems
class CPU:
    def freeze(self): pass
    def execute(self): pass

class Memory:
    def load(self): pass

class HardDrive:
    def read(self): pass

# Facade
class ComputerFacade:
    def __init__(self):
        self.cpu = CPU()
        self.memory = Memory()
        self.hard_drive = HardDrive()
    
    def start(self):
        self.cpu.freeze()
        self.memory.load()
        self.hard_drive.read()
        self.cpu.execute()

# Usage
computer = ComputerFacade()
computer.start()  # Simple interface!

Behavioral Patterns

7. Observer

Objects notify subscribers about changes

Use case: Event systems, pub/sub, MVC

class Subject:
    def __init__(self):
        self._observers = []
    
    def attach(self, observer):
        self._observers.append(observer)
    
    def notify(self, message):
        for observer in self._observers:
            observer.update(message)

class EmailObserver:
    def update(self, message):
        print(f"Email: {message}")

class SMSObserver:
    def update(self, message):
        print(f"SMS: {message}")

# Usage
subject = Subject()
subject.attach(EmailObserver())
subject.attach(SMSObserver())
subject.notify("New order received!")
# Email: New order received!
# SMS: New order received!

8. Strategy

Define family of algorithms, make them interchangeable

Use case: Different sorting algorithms, payment methods, compression

class PaymentStrategy:
    def pay(self, amount):
        pass

class CreditCardPayment(PaymentStrategy):
    def pay(self, amount):
        print(f"Paid ${amount} with credit card")

class PayPalPayment(PaymentStrategy):
    def pay(self, amount):
        print(f"Paid ${amount} with PayPal")

class ShoppingCart:
    def __init__(self, payment_strategy):
        self.payment_strategy = payment_strategy
    
    def checkout(self, amount):
        self.payment_strategy.pay(amount)

# Usage
cart = ShoppingCart(CreditCardPayment())
cart.checkout(100)

cart = ShoppingCart(PayPalPayment())
cart.checkout(50)

9. Command

Encapsulate requests as objects

Use case: Undo/redo, task queues, transactions

class Light:
    def turn_on(self):
        print("Light ON")
    
    def turn_off(self):
        print("Light OFF")

class Command:
    def execute(self):
        pass

class TurnOnCommand(Command):
    def __init__(self, light):
        self.light = light
    
    def execute(self):
        self.light.turn_on()

class TurnOffCommand(Command):
    def __init__(self, light):
        self.light = light
    
    def execute(self):
        self.light.turn_off()

class RemoteControl:
    def __init__(self):
        self.history = []
    
    def execute(self, command):
        command.execute()
        self.history.append(command)

# Usage
light = Light()
remote = RemoteControl()
remote.execute(TurnOnCommand(light))
remote.execute(TurnOffCommand(light))

10. Template Method

Define algorithm skeleton, let subclasses override steps

Use case: Frameworks, game engines, workflows

from abc import ABC, abstractmethod

class DataProcessor(ABC):
    def process(self):
        self.read_data()
        self.process_data()
        self.save_data()
    
    @abstractmethod
    def read_data(self):
        pass
    
    @abstractmethod
    def process_data(self):
        pass
    
    def save_data(self):
        print("Saving to database")  # Common implementation

class CSVProcessor(DataProcessor):
    def read_data(self):
        print("Reading CSV")
    
    def process_data(self):
        print("Processing CSV data")

class JSONProcessor(DataProcessor):
    def read_data(self):
        print("Reading JSON")
    
    def process_data(self):
        print("Processing JSON data")

# Usage
processor = CSVProcessor()
processor.process()

Anti-Patterns (What to Avoid)

God Object: One class does everything Spaghetti Code: Tangled control flow Golden Hammer: Using same solution for every problem Premature Optimization: Optimizing before measuring Copy-Paste Programming: Duplicating code instead of abstracting


When to Use What

System Design Focus Areas:

  • Read-heavy: Caching, read replicas, CDN
  • Write-heavy: Message queues, sharding, async processing
  • Real-time: WebSockets, push notifications, streaming
  • Analytics: Data warehouses, batch processing, OLAP

Design Pattern Selection:

  • Need one instance? → Singleton
  • Complex object creation? → Builder or Factory
  • Add features dynamically? → Decorator
  • Different algorithms? → Strategy
  • Notify multiple objects? → Observer
  • Simplify complex system? → Facade

How useful was this post?

Click on a star to rate it!

Average rating 5 / 5. Vote count: 1

No votes so far! Be the first to rate this post.

As you found this post useful...

Follow us on social media!

We are sorry that this post was not useful for you!

Let us improve this post!

Tell us how we can improve this post?

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *