Hello! Welcome back to your course on System Design.
In our last lesson, we mastered the art of creating UML class diagrams, giving us a powerful visual language to represent our object-oriented designs. We learned how to draw the blueprints for a system, detailing classes, their attributes, methods, and the intricate relationships connecting them.
Today, we move from describing a design to actively improving it. This lesson addresses the learning outcome: Incorporate relevant design patterns to solve common design problems within the model. We'll explore how to leverage time-tested solutions—design patterns—to build more flexible, robust, and maintainable systems. This is a critical skill for any software engineer and a key focus in low-level design interviews.
By the end of this lesson, you will be able to:
- Identify common design problems that signal the need for a pattern.
- Understand how patterns like Strategy, State, Singleton, and Factory solve these problems.
- Apply these patterns within a design model, using the LLD case studies you're interested in.
- Visualize these patterns using the UML notation you've just learned.
1. What Are "Common Design Problems"?
Before memorizing a catalog of patterns, it's crucial to understand the problems they solve. Design patterns are not solutions in search of a problem; they are elegant, reusable solutions to recurring challenges we face when designing software.
Consider these common scenarios during LLD:
- Inflexible Algorithms: You're building a feature that requires a specific algorithm, like calculating a price or assigning a resource. But what happens when the business wants to add a new pricing rule or a different assignment logic? If your logic is hardcoded, you'll be stuck in a cycle of modifying existing code, which is risky and violates the Open/Closed Principle.
- Complex State-Dependent Behavior: An object behaves very differently depending on its internal state. For example, a
VendingMachineacts differently when it'sIdle,AcceptingMoney, orDispensingItem. Managing this with largeif/elseorswitchstatements makes the code complex, hard to read, and difficult to extend with new states. - Uncontrolled Object Creation: Some components, like a system configuration manager, a logger, or a connection pool, should only ever have one instance in the entire application. How do you enforce this and provide a single, global point of access without resorting to global variables?
- Coupled Object Instantiation: Your code needs to create objects, but you don't want it to be tightly coupled to the specific
new MyConcreteClass()constructor calls. This coupling makes it hard to change the type of object being created without modifying the client code.
These are precisely the kinds of challenges where design patterns shine.
2. Applying Patterns: A Case Study Approach
The best way to learn patterns is to see them in action. We'll use the Parking Lot and Elevator System case studies to see how specific patterns solve the problems we just discussed.
First, to get a quick overview of the most common patterns and their Java implementations, let's turn to a helpful article.
Top 10 Most Used Design Patterns in Low-Level Design (LLD)
This article from Medium provides a concise summary of the top 10 design patterns used in LLD. It's a great reference to have on hand.
Skim through this article to familiarize yourself with the three main categories (Creational, Structural, Behavioral) and the basic purpose of patterns like Singleton, Factory, Strategy, and Observer. Pay attention to the simple Java code examples. We will refer back to these patterns as we see them in our case studies.
Now, let's dive into our first case study.
Case Study 1: The Parking Lot System
Remember the Parking Lot problem? Beyond just entities and relationships, a real system needs to handle dynamic logic for allocation, payment, and overall system management. The "Think Software" video on this topic does an excellent job of showing where patterns fit in.
Parking Lot Design | Grokking The Object Oriented Design Interview Question
This video on designing a parking lot system explicitly discusses several design patterns. We'll focus on how they solve specific functional requirements.
Please watch the specified segments, focusing on the 'why' behind each pattern: Strategy Pattern for Spot Assignment (16:34 - 18:20): Notice how an interface, ParkingAssignmentStrategy, is proposed to handle finding a parking spot. This decouples the spot-finding algorithm (e.g., 'nearest to entrance') from the system that uses it. Strategy Pattern for Payments & Tariffs (22:10 - 23:41): The same pattern is applied to PaymentProcessor and TariffCalculator. This allows the system to easily support new payment methods (Apple Pay) or new pricing rules (weekend vs. weekday) without changing existing code. Singleton and Factory Patterns (24:16 - 26:35): The video explains that the ParkingLot class itself should be a Singleton because there is only one system instance. It also suggests using a Factory to create the system's various components (terminals, strategies, etc.) based on a configuration file. This is a great example of centralizing and decoupling object creation.
Key Takeaways from the Parking Lot:
-
Strategy Pattern: Used to encapsulate a family of algorithms and make them interchangeable. We saw it used for:
ParkingAssignmentStrategyPaymentStrategyTariffCalculationStrategy
This is a direct solution to the "Inflexible Algorithms" problem.
-
Singleton Pattern: Used to ensure a class has only one instance.
ParkingLot
This solves the "Uncontrolled Object Creation" problem for a core system component.
-
Factory Pattern: Used to delegate object creation.
- Creating different types of terminals, printers, or payment processors based on configuration.
This solves the "Coupled Object Instantiation" problem.
- Creating different types of terminals, printers, or payment processors based on configuration.
Case Study 2: The Elevator System
The elevator system is a classic LLD problem because it's all about managing state and scheduling—two areas ripe for design patterns.
One of the most important parts of an elevator system is the scheduling algorithm that decides which elevator to send. As the video explains, there are many ways to do this.
Elevator System Design | Grokking the Object Oriented System Design Interview Question
This video on the Elevator System design highlights different scheduling algorithms. This is a perfect scenario for the Strategy pattern.
Watch the segment from 20:06 to 37:47, which discusses various scheduling algorithms (FCFS, SCAN, LOOK). As you watch, think about how you would design a system that could switch between these algorithms. Then, watch the conclusion from 41:47 to 42:23, where the narrator explicitly states that the Strategy pattern is used to implement these dispatching algorithms.
This is a powerful demonstration of the Strategy pattern. Instead of a massive if-else block to select an algorithm, you define a SchedulingStrategy interface and create concrete implementations like NearestElevatorStrategy or LookAlgorithmStrategy. The main ElevatorController can then be configured with any of these strategies.
Here is a UML diagram showing exactly that. Notice the SchedulingStrategy interface and its concrete implementations. This is the Strategy pattern visualized.

Another core challenge in the elevator design is managing its state. An elevator can be Idle, MovingUp, MovingDown, etc. This is the "State-Dependent Behavior" problem we discussed. The State Pattern is the ideal solution.
This article from Official CTO provides a clear explanation and a full Java implementation of an elevator system using the State pattern.
Read through the sections 'Design Patterns', 'Architecture', 'Code Example', and the final summary with the 'UML Diagram'. Notice how the ElevatorState interface defines a contract for all states. See how concrete states (IdleState, MovingUpState) implement this interface. Observe how the main Elevator class holds a reference to the current state object and delegates calls to it. Instead of the elevator having a large switch statement, the state object itself handles the logic.
The State pattern allows an object to change its behavior when its internal state changes. The object appears to change its class. This makes your design clean, extensible, and compliant with the Open/Closed Principle—adding a new state just means adding a new class, not modifying an existing one.
Test your understanding!
Imagine you are designing an e-commerce platform's shipping module. The system needs to calculate shipping costs, but the calculation method depends on the chosen carrier (e.g., FedEx, UPS, DHL). Each carrier has a different API and cost structure. To make matters worse, the business plans to add more shipping partners in the future.
Which design pattern would be most appropriate to handle the shipping cost calculation, and why?
Show answer
The Strategy pattern would be the most appropriate choice.
Why?
- Encapsulates Variation: Each shipping carrier's calculation logic is a different "algorithm" or "strategy." You can create a
ShippingStrategyinterface with acalculateCost()method. - Promotes Flexibility: You would then create concrete classes like
FedExShippingStrategy,UPSShippingStrategy, andDHLShippingStrategy, each implementing the interface. - Follows Open/Closed Principle: When a new carrier like USPS is added, you simply create a new
USPSShippingStrategyclass. You don't need to modify the mainShoppingCartorOrderclass that uses the strategy. The client code remains closed for modification but open for extension.
The ShoppingCart would hold a reference to a ShippingStrategy object and simply call shippingStrategy.calculateCost(), delegating the actual calculation to the selected strategy object.
Conclusion
Excellent work today! You've taken a significant step from simply modeling a system to designing one that is flexible and robust. By incorporating design patterns, you are leveraging the collective wisdom of the software engineering community to solve common problems effectively.
Key Takeaways:
- Problem First, Pattern Second: Don't try to force patterns into your design. First, identify the design problem (e.g., inflexible algorithm, complex state logic), then select the pattern that provides the best solution.
- Strategy Pattern: Use it to encapsulate interchangeable algorithms or behaviors. Think of it whenever you have a component that can do the same thing in different ways.
- State Pattern: Use it to manage an object whose behavior is heavily dependent on its internal state. It's a great way to clean up complex conditional logic.
- Singleton Pattern: Use it to ensure a single instance of a class, perfect for global resources like controllers or configuration managers.
- Factory Pattern: Use it to decouple a client from the concrete classes it needs to instantiate, making your system easier to configure and extend.
In our next lesson, we will continue to refine our design process. We will learn to refine the design by applying SOLID principles and identifying design smells. You'll see that many of the patterns we discussed today are, in fact, concrete implementations of these fundamental principles.