System Design · Lesson 5

How to Solve Design Problems

Whether the prompt is “design a notification service” or “design a large API,” a consistent decision process is more valuable than memorizing one architecture.

Step 1: define the product boundary

Choose the two or three core use cases and explicitly defer secondary features. Clarify who uses the system, expected latency, availability, data retention and security constraints.

Step 2: estimate scale

Approximate requests per second, read/write ratio, object size and storage growth. Use round numbers. The purpose is to identify orders of magnitude, not to produce accounting-grade forecasts.

Step 3: define APIs and data

Sketch endpoints or event contracts and the major entities. Ask which operations require transactions, uniqueness, search, ordering or high write throughput. These access patterns drive storage choices.

Step 4: build the simplest path

Start with clients, load balancing, stateless services and a database. Then add caches, queues, replicas, object storage, search indexes or separate services only when a requirement creates a reason.

Step 5: follow one request end to end

Walking through a read and a write reveals missing components, authentication decisions, latency risks and consistency assumptions. For asynchronous work, describe what happens before and after the queue.

Step 6: attack the bottlenecks

Discuss database hot spots, large fan-out, cache stampedes, retry storms, slow dependencies and skewed traffic. Explain the mitigation and the new complexity it introduces.

Step 7: cover production concerns

Add timeouts, idempotency, observability, rate limiting, access control, backups and disaster recovery at the level appropriate to the prompt. Avoid listing technologies without explaining why they are needed.

Example: notification service

A basic design might accept a notification request through an API, persist an idempotency key, publish work to a queue, have channel-specific workers send email/SMS/push, record delivery state and expose status. At scale you may partition queues, rate-limit providers, retry transient errors and move failed work to a dead-letter queue.

The strongest answer is a conversation: state assumptions, explain trade-offs and adjust the design as new constraints appear.