DEV Community

Vikash Kumar
Vikash Kumar

Posted on

I'm Exploring Low Level Design , and SOLID Is Changing How I Look At Code

I've started going deeper into Low-Level Design as part of my journey toward becoming a stronger software engineer.

My initial understanding of LLD was mostly around classes, objects, relationships, and design patterns.

But one thing became clear very quickly:

Good LLD isn't about writing more classes or using more design patterns.

It's about making code easier to change without creating unnecessary complexity.

That's where SOLID started making more sense to me.

SOLID isn't five rules to memorize

The five principles are:

Single Responsibility Principle
Open/Closed Principle
Liskov Substitution Principle
Interface Segregation Principle
Dependency Inversion Principle

But instead of memorizing their definitions, I'm trying to understand the problems they're trying to prevent.

1. SRP — How many reasons can make this class change?_

Imagine an Invoice class responsible for:

  • calculating the invoice
  • printing the invoice
  • saving it to the database

It may work perfectly today.

But these responsibilities can change independently.

A change in calculation logic shouldn't necessarily require touching printing logic.

A database change shouldn't necessarily affect invoice calculation.

That made the idea of SRP much clearer to me:

The important question isn't simply "How many things does this class do?"

It's "How many independent reasons can make this class change?"

2. OCP — What happens when a new requirement arrives?

Suppose an invoice can currently be saved to a database.

Tomorrow the requirement becomes:

"We also need to save invoices to files."

One approach is to keep adding methods to the existing class.

But as requirements continue to grow, the class becomes a collection of unrelated variations.

Using an abstraction allows different implementations to provide different behaviors.

This made OCP feel less like:

"Never modify existing code."

and more like:

"Design stable parts so that new behavior can be added without constantly changing them."

3. LSP — Inheritance is more than code reuse

The Liskov Substitution Principle was particularly interesting to me.

Imagine a Vehicle abstraction that assumes every vehicle has an engine.

A car fits.

A motorcycle fits.

But a bicycle doesn't.

If client code expects every Vehicle to provide engine-related behavior, substituting a bicycle can break the program's assumptions.

The problem isn't simply that the child class has a different implementation.

The problem is that the parent abstraction made a promise that the child cannot satisfy.

That gave me a better way to think about inheritance:

A subtype should preserve the behavioral expectations of its parent.

4. ISP — Don't make clients implement what they don't need

Imagine one RestaurantEmployee interface containing:

  • washDishes()
  • serveCustomers()
  • cookFood()

A waiter shouldn't need to implement cooking and dishwashing just because those methods happen to exist on a large interface.

Breaking the interface into smaller, focused interfaces makes the dependency more meaningful.

The lesson:

An abstraction should be focused enough that its clients actually need what it exposes.

5. DIP — Depend on capability, not implementation

What I'm taking away

The biggest change in my thinking isn't that I now know what SOLID stands for.

It's that I'm starting to look at code by asking:

How easily can this code change?

When I see a class, I want to ask:

  • What can cause it to change?
  • Is it taking too many responsibilities?
  • What happens when a new behavior is introduced?
  • Can its subclasses really substitute for it?
  • Are clients depending on things they don't need?
  • Am I coupling the design to implementation details?

I still have a lot to learn.

Next, I'm going deeper into design patterns and LLD problems to understand how these principles translate into actual designs and code.

I'm documenting that journey as I go.

Consider a MacBook that directly creates:

WiredKeyboard
WiredMouse

The class now knows too much about concrete implementations.

If we instead make it depend on:

Keyboard
Mouse

we can provide different implementations without changing the MacBook itself.

This is where dependency inversion starts becoming practical:

High-level logic shouldn't be tightly coupled to low-level implementation details.

Top comments (0)