Loading learning content...
There's a pervasive myth in software engineering: the idea that experienced designers sit down, think deeply for a moment, and then produce a near-perfect design in a single pass. This myth is not just misleading—it's actively harmful to developing genuine design skill.
The reality is radically different. Every elegant, well-structured system you've ever admired went through dozens, sometimes hundreds, of iterations before reaching its final form. The clean architecture of a booking system, the intuitive API of a payment gateway, the extensible design of a rules engine—all of these emerged through a process of progressive refinement, not divine inspiration.
This page establishes a fundamental truth that will transform how you approach Low-Level Design: design is inherently iterative, and mastering the iteration process is as important as mastering design patterns or SOLID principles.
By the end of this page, you will understand why iteration is not a sign of weakness but a source of strength in design. You'll learn the psychology behind effective iteration, practical techniques for evolving designs, and how to structure your iteration process to maximize both learning and design quality.
Before we discuss techniques, we need to address the psychology that makes iteration difficult for many engineers. Understanding these mental barriers is the first step to overcoming them.
Why we resist iteration:
Many developers feel that needing to iterate indicates failure or inadequacy. We've been trained to value getting things right the first time, and iteration can feel like admitting we got it wrong. This mindset is not just incorrect—it's the primary obstacle to becoming a better designer.
Consider what actually happens during design:
The best designers iterate more, not less. Their willingness to revisit and refine is what produces elegant results. Each iteration represents learning applied. An engineer who iterates three times and produces an excellent design has demonstrated more skill than one who stops at their first attempt because they're afraid to admit it could be better.
The growth mindset in design:
Psychologist Carol Dweck's research on growth mindset applies directly to design work. Engineers with a growth mindset view design challenges as opportunities to learn, while those with a fixed mindset see them as tests of their innate ability. This distinction has profound implications:
The growth mindset isn't just more pleasant—it's more accurate. Design is a skill that develops through practice and feedback, not an innate talent you either have or don't.
Not all iterations are created equal. A productive iteration follows a structured pattern that maximizes learning and minimizes wasted effort. Understanding this anatomy helps you iterate more effectively.
The Four Phases of Effective Iteration:
Every productive design iteration moves through four distinct phases. Skipping phases leads to shallow iterations that don't actually improve the design.
| Phase | Activities | Key Questions | Output |
|---|---|---|---|
| Analyze current design against requirements and principles | Does this design meet functional requirements? Are SOLID principles violated? What are the pain points? | List of specific issues and concerns |
| Generate potential improvements | What patterns might address this issue? Are there alternatives? What are the trade-offs? | Set of proposed changes with rationale |
| Implement the most promising change | How does this change affect other components? Does it introduce new issues? | Updated design artifacts |
| Assess the impact of the change | Is the design better? Did we introduce new problems? What did we learn? | Lessons learned, next iteration decision |
Example: Iterating on a Notification System Design
Let's walk through a concrete example to see these phases in action.
Initial Design (Iteration 0):
You're designing a notification system for an e-commerce platform. Your first design has a single NotificationService class that handles email, SMS, and push notifications directly.
1234567891011121314151617181920212223242526272829303132333435363738
// Initial design: Single class handling all notification typesclass NotificationService { private emailClient: EmailClient; private smsClient: SMSClient; private pushClient: PushNotificationClient; constructor( emailClient: EmailClient, smsClient: SMSClient, pushClient: PushNotificationClient ) { this.emailClient = emailClient; this.smsClient = smsClient; this.pushClient = pushClient; } sendNotification( userId: string, type: 'email' | 'sms' | 'push', message: string ): void { const user = this.getUser(userId); switch (type) { case 'email': this.emailClient.send(user.email, message); break; case 'sms': this.smsClient.send(user.phone, message); break; case 'push': this.pushClient.send(user.deviceToken, message); break; } } private getUser(userId: string): User { /* ... */ }}Iteration 1: Applying Evaluate-Hypothesize-Apply-Reflect
Evaluate Phase: Looking at this design, several issues emerge:
NotificationServiceHypothesize Phase: Potential improvements:
NotificationChannel interface with implementations per channelApply Phase: Let's refactor using the Strategy Pattern:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
// Iteration 1: Strategy Pattern for notification channels interface NotificationChannel { send(user: User, message: string): void; supports(user: User): boolean;} class EmailChannel implements NotificationChannel { constructor(private emailClient: EmailClient) {} send(user: User, message: string): void { this.emailClient.send(user.email, message); } supports(user: User): boolean { return user.email !== null; }} class SMSChannel implements NotificationChannel { constructor(private smsClient: SMSClient) {} send(user: User, message: string): void { this.smsClient.send(user.phone, message); } supports(user: User): boolean { return user.phone !== null; }} class PushChannel implements NotificationChannel { constructor(private pushClient: PushNotificationClient) {} send(user: User, message: string): void { this.pushClient.send(user.deviceToken, message); } supports(user: User): boolean { return user.deviceToken !== null; }} // NotificationService now uses channels through abstractionclass NotificationService { constructor(private channels: NotificationChannel[]) {} sendNotification(user: User, message: string): void { for (const channel of this.channels) { if (channel.supports(user)) { channel.send(user, message); } } }}Reflect Phase:
What improved:
NotificationService (OCP satisfied)What new questions arose:
These questions seed the next iteration. Notice that the act of improving the design revealed new considerations that weren't visible before.
A key insight is that iteration doesn't just improve your design—it improves your understanding of the problem. The questions that arise during reflection often reveal requirements or edge cases that no one explicitly stated but that will definitely matter in production.
Iteration is valuable, but it's not infinite. One of the skills that separates experienced designers from novices is knowing when to continue iterating and when to declare the design "good enough."
Signals that indicate you should continue iterating:
Signals that indicate you can stop iterating:
Perfectionism disguised as thoroughness can trap you in endless iteration. Remember: the goal is a design that's good enough to implement, not one that's theoretically perfect. Real-world feedback from implementation will inform further refinement. A shipped design that works is infinitely more valuable than a perfect design that's never built.
The 80/20 Rule in Design Iteration:
Typically, the first few iterations produce the most dramatic improvements. The first iteration might triple the design quality. The second iteration might improve it by 50%. By the fifth iteration, you're often making 5% improvements at best.
Recognizing this diminishing returns curve helps you allocate effort appropriately. For a critical core component, those small improvements might be worth pursuing. For a peripheral utility, stopping earlier is often wiser.
| Component Type | Suggested Iteration Depth | Rationale |
|---|---|---|
| Core domain entities | High (5+ iterations) | Central to the system; mistakes are costly; frequently touched code |
| Primary business logic | High (4-5 iterations) | Directly impacts user value; needs to be correct and maintainable |
| Infrastructure adapters | Medium (2-3 iterations) | Important but often replaceable; follow established patterns |
| Utility classes | Low (1-2 iterations) | Peripheral to core concerns; over-engineering is common trap |
| Prototypes/experiments | Minimal (1 iteration) | Meant to be thrown away; quick feedback is the goal |
Having established the philosophy and timing of iteration, let's explore practical techniques that make iteration more effective.
Technique 1: The Refactoring Mindset
Approach design iteration the same way you approach code refactoring: small, safe steps that preserve behavior while improving structure. This mindset prevents the common failure mode of "let me just start over," which often loses valuable learning embedded in the current design.
Technique 2: Design Spikes
Sometimes you're uncertain which of several approaches would work best. Rather than picking one and hoping, consider running a design spike: a quick, time-boxed exploration of multiple alternatives.
This technique prevents the sunk cost fallacy—the tendency to stick with an approach because you've already invested time in it, even when an alternative would be better.
During design spikes, resist the urge to fully detail each alternative. You're exploring, not committing. A rough sketch that captures the key abstraction and a few class names is enough to evaluate an approach. Save the detail work for after you've chosen your direction.
Technique 3: Role-Playing the Design
One powerful iteration technique is to mentally "execute" your design by walking through scenarios as if you were the code. For each scenario:
This technique is especially valuable for catching:
Technique 4: The "Explain to a Colleague" Test
Imagine explaining your design to a colleague who's smart but unfamiliar with the specifics. As you mentally walk through the explanation:
These points often indicate areas that need iteration. If you can't explain why a decision was made, it may not have been a good decision.
Technique 5: Future Extension Simulation
Identify three likely future requirements (based on product roadmap, industry patterns, or reasonable speculation). Mentally trace how your design would accommodate each:
If accommodating these extensions would require major surgery, consider whether the current design adequately separates concerns and provides appropriate extension points.
Don't over-engineer for unlikely futures, but do ensure your design doesn't paint you into a corner for likely ones. The goal isn't to anticipate every possible change but to ensure that common evolution patterns can be accommodated without rewriting the core design.
Just as there are productive iteration patterns, there are anti-patterns that waste time and often make designs worse. Recognizing these helps you avoid falling into them.
| Healthy Sign | Unhealthy Sign |
|---|---|
| Each iteration reduces complexity or increases clarity | Iterations add more classes without clear benefit |
| You can articulate what each iteration accomplished | You can't explain why you made recent changes |
| Changes are getting smaller as design stabilizes | Changes are getting larger, indicating fundamental issues |
| You're addressing issues from evaluation criteria | You're making changes based on vague feelings |
| The design is easier to test after each iteration | Testing considerations aren't improving |
| You feel more confident explaining the design | You feel increasingly uncertain about justify decisions |
If you notice multiple unhealthy signs, it may be time to step back rather than continue iterating. Question your fundamental assumptions: Are the entities correct? Is the core abstraction sound? Sometimes the bravest iteration is acknowledging that the foundation needs rethinking.
LLD interviews, despite their time constraints, expect and reward iterative thinking. Interviewers are evaluating not just your final design but your ability to evolve it based on feedback and changing requirements.
How interviews simulate iteration:
Typical interview dynamics that test your iteration skills:
Demonstrating iterative thinking in interviews:
Start simple, then elaborate: Begin with a straightforward design and explicitly state that you'll refine it. This sets the expectation that iteration is coming.
Verbalize your evaluation: As you review your design, say things like "Looking at this, I notice that adding a new notification type would require modifying this class, which violates Open/Closed. Let me refine this..."
Propose before implementing: Before making a change, briefly explain what you're about to do and why. "I'm going to extract an interface here so that we can swap implementations for testing."
Embrace interviewer feedback: When an interviewer suggests a change or asks about an extension, treat it as valuable input for iteration, not criticism.
Time-box your iterations: In a 45-minute interview, you might go through 2-3 major design iterations. Don't spend 40 minutes on the first version—leave time to show you can refine.
A candidate who starts with a basic design and thoughtfully iterates to a solid solution often scores higher than one who presents a complex design immediately. The iteration demonstrates problem-solving process, which is what interviewers are really evaluating.
We've covered significant ground on why and how to iterate effectively. Let's consolidate the key takeaways:
What's Next:
Now that we understand that design is iterative, the next page focuses on a critical input to iteration: feedback. We'll explore how to gather, evaluate, and incorporate feedback from various sources—whether from colleagues, stakeholders, or the requirements themselves—to drive productive iterations.
You now understand that iterative design is not a sign of weakness but the hallmark of professional engineering practice. In the next page, we'll examine how to systematically incorporate feedback into your design iterations to accelerate learning and improve quality.