Low level design (LLD) in Java is the process of turning a high-level requirement into concrete classes, interfaces, and interactions that can be directly coded. For a beginner, modeling a working system means identifying the core objects, their responsibilities, and how they communicate, then writing Java code that reflects those decisions. This article walks through a practical, step-by-step approach to LLD using a real example, so you can move from a vague idea to a compilable, testable system.
Formula
The calculator above estimates parking lot utilization and the number of cars served, assuming each spot is occupied 60% of the time on average. The formula is:
Average utilization (%) = (occupied spots / total spots) × 100, where occupied spots = total spots × 0.6.
Cars served = total spots × 0.6 × (duration in hours / 1 hour per car).
Variables: t = total parking spots (integer), d = simulation duration in hours (integer). The 0.6 factor is a placeholder for a typical occupancy rate; in a real LLD simulation, this would come from your car arrival logic.
Worked Example
Using the defaults: t = 50 spots, d = 24 hours.
- Average utilization = (50 × 0.6 × 100) / 50 = 60%.
- Cars served = 50 × 0.6 × (24 / 1) = 30 × 24 = 720 cars.
The calculator returns 60% utilization and 720 cars served. This matches the arithmetic.
What Is Low Level Design in Java?
Low level design (also called detailed design or object-oriented design) sits between system architecture (high level) and actual code. While high-level design picks databases, microservices, or message queues, LLD decides the classes, methods, fields, and interfaces inside a single module. In Java, LLD typically involves:
- Identifying entities (e.g., ParkingSpot, Car, ParkingLot).
- Defining relationships (inheritance, composition, association).
- Choosing design patterns (Singleton, Factory, Strategy, etc.).
- Handling concurrency if multiple threads access shared objects.
- Writing unit tests to verify behavior.
For a beginner, the goal is not to produce a perfect UML diagram, but to create a set of Java classes that compile, run, and satisfy the functional requirements.
Step-by-Step: From Requirement to Classes
Let’s model a simple parking lot system. The requirement: “A parking lot has multiple floors, each with spots of different sizes. Cars enter, park, and exit. Track available spots.”
- List nouns and verbs. Nouns: ParkingLot, Floor, Spot, Car, Ticket. Verbs: park, unpark, isAvailable.
- Create a class per noun. Start with
ParkingSpot(has id, size, isOccupied),Floor(has list of spots),ParkingLot(has list of floors). - Add methods for verbs.
ParkingLot.parkCar(Car car)finds an available spot and marks it occupied;unparkCar(Ticket ticket)frees the spot. - Define size as an enum. Use
enum SpotSize { SMALL, MEDIUM, LARGE }to avoid magic strings. - Handle edge cases. What if no spot is available? Throw a custom exception or return a boolean.
- Write a main method or test. Simulate a few cars entering and leaving, print utilization.
This is the essence of LLD: you are not thinking about database tables yet, just Java objects and their interactions.
Key Design Principles for Beginners
Three principles will guide most of your LLD decisions:
- Single Responsibility Principle (SRP): Each class should have one reason to change. For example,
ParkingSpotonly tracks its own state, not pricing. - Open/Closed Principle: You should be able to extend behavior without modifying existing code. Use interfaces like
ParkingStrategyso you can add new pricing rules later. - Composition over inheritance: Prefer has-a relationships. A
ParkingLothasFloorobjects, rather than inheriting from a genericBuilding.
These principles keep your model flexible. For instance, if you later add electric vehicle charging spots, you can create a subclass of ParkingSpot without touching the core logic.
Common Mistakes Beginners Make
Several pitfalls trip up new LLD practitioners:
- Over-modeling: Creating a class for every tiny concept (e.g.,
CarColor) clutters the design. Only model what the requirement needs. - Ignoring null checks: Methods like
findSpot()may return null. Always handle the “not found” case explicitly. - Mixing UI with logic: Keep your model classes free of System.out.println or Swing components. Use a separate view class.
- Forgetting concurrency: If multiple threads simulate cars, your
ParkingSpot.isOccupiedflag needs synchronization or atomic variables. - Skipping tests: A working system means tested behavior. Write JUnit tests for park/unpark, full lot, and invalid ticket.
A common misconception is that LLD requires memorizing design patterns. Patterns are tools, not rules. Start with plain classes, then apply a pattern only when you see repeated code or inflexibility.
How to Choose Between Design Patterns
Patterns solve specific problems. For a beginner, here is a quick guide:
- Singleton: Use when you need exactly one instance, like a
ParkingLotin a single-process app. But avoid it in multithreaded tests; use dependency injection instead. - Factory: Use when creating objects with different configurations. For example, a
SpotFactorythat creates SMALL, MEDIUM, or LARGE spots. - Strategy: Use when you have interchangeable algorithms, like different pricing strategies (hourly, daily, flat).
- Observer: Use when one object needs to notify others, like a
ParkingLotnotifying a display board when spots change.
If you are unsure, write the code without a pattern first. Refactor to a pattern only when you see a clear benefit. Overusing patterns early makes the design harder to read.
Building a Working System: A Minimal Example
Here is a skeleton of what your Java classes might look like (simplified for clarity):
public enum SpotSize { SMALL, MEDIUM, LARGE }
public class ParkingSpot {
private final int id;
private final SpotSize size;
private boolean occupied;
public ParkingSpot(int id, SpotSize size) { this.id = id; this.size = size; }
public boolean isAvailable() { return !occupied; }
public void occupy() { occupied = true; }
public void release() { occupied = false; }
}
public class ParkingLot {
private final List<ParkingSpot> spots;
public ParkingLot(int totalSpots) {
spots = new ArrayList<>();
for (int i = 0; i < totalSpots; i++) {
spots.add(new ParkingSpot(i, SpotSize.MEDIUM));
}
}
public synchronized boolean park() {
for (ParkingSpot spot : spots) {
if (spot.isAvailable()) { spot.occupy(); return true; }
}
return false; // lot full
}
public synchronized void unpark(int spotId) {
spots.get(spotId).release();
}
}
This is a working model. You can test it by creating a ParkingLot with 50 spots, calling park() 60 times, and checking that the 51st call returns false. That is a concrete verification of your design.
Frequently Asked Questions
Do I need to learn UML for low level design?
UML class diagrams are helpful for communicating your design, but not mandatory. Most beginners can start with plain Java code and use simple diagrams only when explaining to others. Focus on the code, not the notation.
What is the difference between high-level and low-level design?
High-level design (HLD) deals with system architecture: modules, databases, APIs, and scalability. Low-level design (LLD) deals with the internals of a module: classes, methods, and interactions. In a job interview, HLD is about designing Twitter, LLD is about designing a parking lot.
How do I practice low level design as a beginner?
Pick a small system like a library, ATM, or vending machine. Write the requirement in one sentence, list nouns and verbs, then code the classes. Test with a simple main method. Repeat with a new system each week, gradually adding complexity like concurrency or patterns.
Is low level design only for interviews?
No. In real projects, LLD is how you structure code before writing it. Good LLD reduces bugs, makes code easier to change, and helps teams collaborate. It is a daily skill for professional Java developers, not just an interview topic.
Should I use records or traditional classes for my models?
Java records (introduced in Java 16) are great for immutable data carriers like a Ticket or Car that have no behavior. For objects with state changes (like ParkingSpot), use a regular class. Mix both as needed.
How do I handle errors in my model?
Use exceptions for exceptional conditions, like LotFullException when parking. For expected checks (e.g., is a spot available), return a boolean or Optional. Avoid returning null; use Optional<ParkingSpot> to make the absence explicit.