Lesson 8 of 21 6 min

The Senior Engineer Guide to LLD: SOLID Principles in Practice

Go beyond definitions. Learn how to apply SOLID principles to build extensible, maintainable, and testable production systems in Java.

Reading Mode

Hide the curriculum rail and keep the lesson centered for focused reading.

Introduction to LLD and SOLID

Mental Model

Applying Staff-level engineering principles to build robust, production-grade software.

graph TD
    JVM[Java Virtual Machine]
    JVM --> Heap[Heap Memory]
    JVM --> Stack[Thread Stacks]
    JVM --> Metaspace[Metaspace]
    Heap --> Eden[Young Gen: Eden]
    Heap --> Survivor[Young Gen: Survivor]
    Heap --> Old[Old Generation]

In System Design (HLD), we worry about shards, replication, and availability. In Low-Level Design (LLD), we worry about change.

A poor LLD means a simple requirement change requires touching 15 files. A good LLD means adding a new feature is as simple as adding a new class. The bridge between these two worlds is the SOLID principles.

Most engineers can recite what SOLID stands for. Few can apply them to a production codebase without over-engineering. This guide focuses on the practical application of SOLID in modern Java backend services.

1. Single Responsibility Principle (SRP)

"A class should have one, and only one, reason to change."

The Violation: A UserService that handles user validation, database persistence, email notifications, and JWT token generation.

The Fix: Break the service down.

  • UserValidator: Handles business rules.
  • UserRepository: Handles database interaction.
  • NotificationService: Handles communication.
  • TokenProvider: Handles security tokens.

Senior Tip: If you find yourself using the word "and" when describing what your class does, you are likely violating SRP.

2. Open/Closed Principle (OCP)

"Software entities should be open for extension, but closed for modification."

The Violation: Using a large switch statement to handle different payment methods. Every time you add "Crypto", you have to modify the existing PaymentProcessor class.

The Fix: The Strategy Pattern.

public interface PaymentStrategy {
    void process(double amount);
}

public class PaymentProcessor {
    private final Map<String, PaymentStrategy> strategies;

    public void executePayment(String type, double amount) {
        strategies.get(type).process(amount);
    }
}

Now, adding a new payment method requires adding a new class, zero changes to the PaymentProcessor.

3. Liskov Substitution Principle (LSP)

"Objects of a superclass should be replaceable with objects of its subclasses without breaking the application."

The Violation: A Square class extending a Rectangle class where setting the width also changes the height. This breaks the expectation of the client code using the Rectangle interface.

The Fix: Favor composition over inheritance or ensure that the subclass strictly adheres to the contract of the parent. If a subclass throws an UnsupportedOperationException for a method defined in the parent, you are likely violating LSP.

4. Interface Segregation Principle (ISP)

"Clients should not be forced to depend upon interfaces that they do not use."

The Violation: A Worker interface with work() and eat(). If you have a RobotWorker, it is forced to implement eat(), which it doesn't need.

The Fix: Split the interfaces.

  • Workable { void work(); }
  • Feedable { void eat(); }

Senior Tip: In Java, default methods in interfaces can be a trap. Ensure they don't force unrelated behavior on implementers.

5. Dependency Inversion Principle (DIP)

"Depend upon abstractions, not concretions."

The Violation: OrderService directly instantiating MySQLUserRepository. If you want to switch to MongoDB, you have to modify OrderService.

The Fix: Dependency Injection.

public class OrderService {
    private final UserRepository repository; // Interface, not implementation

    public OrderService(UserRepository repository) {
        this.repository = repository;
    }
}

By injecting the dependency (usually via Spring's @Autowired or constructor injection), OrderService remains agnostic of the underlying database technology.

When to Ignore SOLID?

Over-applying SOLID leads to "Class Explosion"—a codebase with thousands of tiny, 5-line classes that are impossible to navigate.

The Rule of Three: Don't apply OCP or ISP the first time you write a feature. Apply it the third time you have to modify that area of code. Premature abstraction is the root of all evil in LLD.

Final Takeaways

  • SRP keeps your classes small and testable.
  • OCP makes your system pluggable.
  • LSP ensures your abstractions are honest.
  • ISP prevents "Fat Interfaces."
  • DIP decouples your business logic from your infrastructure.

Engineering Standard: The "Staff" Perspective

In high-throughput distributed systems, the code we write is often the easiest part. The difficulty lies in how that code interacts with other components in the stack.

1. Data Integrity and The "P" in CAP

Whenever you are dealing with state (Databases, Caches, or In-memory stores), you must account for Network Partitions. In a standard Java microservice, we often choose Availability (AP) by using Eventual Consistency patterns. However, for financial ledgers, we must enforce Strong Consistency (CP), which usually involves distributed locks (Redis Redlock or Zookeeper) or a strictly linearizable sequence.

2. The Observability Pillar

Writing logic without observability is like flying a plane without a dashboard. Every production service must implement:

  • Tracing (OpenTelemetry): Track a single request across 50 microservices.
  • Metrics (Prometheus): Monitor Heap usage, Thread saturation, and P99 latencies.
  • Structured Logging (ELK/Splunk): Never log raw strings; use JSON so you can query logs like a database.

3. Production Incident Prevention

To survive a 3:00 AM incident, we use:

  • Circuit Breakers: Stop the bleeding if a downstream service is down.
  • Bulkheads: Isolate thread pools so one failing endpoint doesn't crash the entire app.
  • Retries with Exponential Backoff: Avoid the "Thundering Herd" problem when a service comes back online.

Critical Interview Nuance

When an interviewer asks you about this topic, don't just explain the code. Explain the Trade-offs. A Staff Engineer is someone who knows that every architectural decision is a choice between two "bad" outcomes. You are picking the one that aligns with the business goal.

Performance Checklist for High-Load Systems:

  1. Minimize Object Creation: Use primitive arrays and reusable buffers.
  2. Batching: Group 1,000 small writes into 1 large batch to save I/O cycles.
  3. Async Processing: If the user doesn't need the result immediately, move it to a Message Queue (Kafka/SQS).

Key Takeaways

  • UserValidator: Handles business rules.
  • UserRepository: Handles database interaction.
  • NotificationService: Handles communication.

Verbal Interview Script

Interviewer: "How does the JVM handle memory allocation for this implementation, and what are the GC implications?"

Candidate: "In this implementation, the short-lived objects are allocated in the Eden space of the Young Generation. Because they have a very short lifecycle, they will be quickly collected during a Minor GC, which is highly efficient. However, if we were to maintain strong references to these objects—for instance, in a static Map or a long-lived cache—they would survive multiple GC cycles and get promoted to the Old Generation. This would eventually trigger a Major GC (or Full GC), causing a "Stop-the-World" pause that increases our P99 latency. To mitigate this in a high-throughput environment, I would consider using the ZGC or Shenandoah garbage collectors for predictable sub-millisecond pause times, or optimize the data structures to reduce object churn."

Want to track your progress?

Sign in to save your progress, track completed lessons, and pick up where you left off.