When most people think about system design, they imagine designing a system that can handle millions of users. Conversations quickly turn into load balancers, distributed databases, caching layers, and scalability.
I used to think that was what system design was about too.
After working on real projects, I've learned that the hardest part of system design usually has nothing to do with scaling. More often than not, you're handed a vaguely defined business problem, a desired outcome, and are expected to figure out everything in between.
These are the three lessons I wish I had learned earlier.
The biggest mistake I see engineers make is jumping straight into solutions before fully understanding the problem.
In interviews, requirements are usually clean and well-defined. In reality, they're often incomplete, contradictory, or ambiguous. A stakeholder might tell you the system needs to support "1,000 users," but what does that actually mean? Is that 1,000 concurrent users? 1,000 users per day? Per minute?
Those details completely change the design.
A large portion of system design isn't designing at all -- it's actually requirement gathering. Talking with product managers, customers, business stakeholders, and other engineers. Asking questions. Identifying edge cases. Clarifying assumptions. Writing everything down.
The quality of your design is limited by the quality of your understanding of the problem.
Before drawing a single diagram, make sure you know exactly what you're building and why.
Once the requirements are clear, most people immediately start thinking about inputs, APIs, and services.
I prefer starting at the opposite end.
What does success look like?
Where is the data supposed to end up? Is it powering a user interface? Feeding a dashboard? Stored in a data warehouse? Sent to another service? How is the user supposed to interact with your system?
Your end state defines the system.
Once you understand the destination, the path becomes much easier to reason about. From there, identify the starting state and work backwards until the two connect.
This approach prevents you from exploring dozens of unnecessary design paths. Instead of designing every possible solution, you're designing the solution that gets your data from its starting point to its intended destination.
A well-defined start state and end state turn a messy problem into a solvable one.
The best system designs are rarely the most complicated ones.
A common trap is overengineering. Engineers often design for problems they don't actually have yet, introducing layers of complexity that make systems harder to build, maintain, and debug.
Start with the simplest design that satisfies your requirements.
Then ask yourself three questions:
Failures are inevitable. Services go down. Messages get dropped. Databases become unavailable. The goal isn't to eliminate failures -- it's to design systems that can tolerate them.
Observability is just as important! If you can't trace requests, monitor system health, or understand where failures occur, debugging becomes a nightmare.
A simple system with good observability and fault tolerance is almost always better than a complex system built around hypothetical future requirements.
Build what you need today. Leave room for growth tomorrow.
Most system design content focuses on scaling.
Scaling is important, but it's only one piece of the puzzle.
The engineers I've learned the most from spend far less time discussing distributed systems and far more time discussing requirements, tradeoffs, failure modes, and business needs.
Before worrying about how your system will handle millions of users, make sure you understand the problem, define the destination, and build something simple enough to evolve.
Everything else becomes much easier after that.