Logo
Technical Article

LLD Interview Master Guide: Generic Patterns, SOLID Principles & Interview Execution

15 min read

For Senior / Staff Backend Engineers


The Core Problem#

Most candidates fail LLD rounds not because they lack knowledge, but because they cannot apply knowledge under time pressure. The fix is not to study more patterns — it is to have a repeatable execution framework that works on any problem.

Root Cause: You know SOLID. You know patterns. But in the room, you start coding immediately, skip the thinking phase, and end up with a ball of mud. The solution is a structured thinking ritual you run before writing a single class.

The Two Failure Modes#

Failure ModeWhat it looks like
Over-engineering5 minutes in, you are debating Event Sourcing. No classes written yet.
Under-engineeringYou write working code but a flat structure — no layers, no interfaces, God classes everywhere.

Both are solved by the same thing: a time-boxed, sequential ritual.


The 5-Minute Thinking Ritual#

Run this before writing any class. Every time. Without exception.

Step 1 — Identify the Entities (60 seconds)#

Ask: "What are the nouns in this system?" These become your models.

  • Write them down: URL, User, Click, Game, Player, Move, Order, Product...
  • Do not worry about fields yet. Just get the nouns on paper.
  • Ignore verbs for now. Verbs are use-cases, not models.

Signal: Identifying 4–6 entities in 60 seconds shows the interviewer you can decompose a problem fast.

Step 2 — Identify the Use-Cases (60 seconds)#

Ask: "What does this system DO?" These become your service methods.

  • Write verbs: shorten URL, resolve URL, register user, make move, place order...
  • Each verb is one service method. If you have 6 verbs, you have 6 service methods.
  • Do not think about HOW yet. Just list what the system does.

Step 3 — Identify What Can Go Wrong (30 seconds)#

Ask: "What are the failure cases?" These become your exceptions.

  • Not found, already exists, not your turn, expired, invalid input...
  • One exception per distinct failure. Name from the caller's perspective.

Step 4 — Draw the Dependency Arrow (30 seconds)#

Always draw this mentally before coding:

main.py
  └── Service        (depends on Interfaces)
        └── Interface  (depends on Models)
              └── Repository  (implements Interface, depends on Models)
                    └── Model  (depends on nothing)

If any arrow points the wrong way — a model importing a service, a repo calling another repo — the design is broken.

Step 5 — State Your Constraints Out Loud (60 seconds)#

Before coding, say one sentence per constraint. This is where staff-level thinking becomes visible.

  • "The redirect path is the hot path — click recording must not block it."
  • "Short URL generation needs to be collision-free and non-enumerable."
  • "Board state changes on every move — I need atomic read-modify-write."

Why this matters: Constraints drive design. An interviewer hearing constraints before code sees an engineer who thinks before building. This is the clearest staff-level signal in an LLD round.


The Universal Layer Map#

Every LLD — regardless of domain — maps to the same six layers. Memorise this once.

LayerQuestion it answers
model_class.pyWhat does my data look like?
exceptions.pyWhat can go wrong?
interfaces.pyWhat operations must every repo support?
repo_class.pyHow is data stored and retrieved?
service_class.pyWhat are the use-cases? What does this system DO?
main.pyHow do I wire everything together?

The One Rule to Never Break#

# Dependencies only point INWARD.
# Outer layers know about inner layers.
# Inner layers NEVER know about outer layers.

main        → can import everything
service     → imports interfaces + models + exceptions
interfaces  → imports models only
repo        → imports interfaces + models
model       → imports nothing
exceptions  → imports nothing

Violation signal: If model_class.py ever imports from service_class.py, or a repo instantiates another repo inside a method, the design has broken the dependency rule.

What Each Layer Must NOT Do#

LayerMust NOT
modelContain business logic, import from any other layer
exceptionsImport from any other layer
interfacesContain implementation, know about concrete repos
repoInstantiate other repos, contain business logic, check expiry
serviceWrite SQL/Redis commands, import concrete repo classes, store state
mainContain business logic, domain validation, if/else for domain rules

SOLID Principles — Practical Application#

Do not recite definitions. Show SOLID through your design choices.

S — Single Responsibility Principle#

One class, one reason to change.

ViolationFix
URLRepository records clicks inside get_url()Click recording is a side-effect. Move it to URLService.
GameService contains all move validation logic (500 lines)Extract MoveValidator. Service orchestrates; validator reasons.
main() contains business logicmain() is a composition root. Move logic to a service.

Interview signal: When you extract MoveValidator from GameService, say it out loud: "I'm extracting this because validation is complex enough to have its own test suite and its own reason to change."

The extraction signal: If you find yourself writing a comment like # --- move validation --- inside a class, that block wants to be its own class.

O — Open/Closed Principle#

Open for extension, closed for modification. Add new behaviour without editing existing classes.

# BAD: adding a new piece type requires editing MoveValidator
def _piece_can_reach(self, piece, to_pos):
    if piece.type == "rook": ...
    if piece.type == "bishop": ...
    if piece.type == "archbishop": ...  # now you edit this class

# GOOD: each piece knows its own movement rules
class Archbishop(Piece):
    def can_reach(self, to_pos): ...    # extend, do not modify

When to apply: A long if/elif/switch on a type field is the signal to replace with polymorphism.

L — Liskov Substitution Principle#

Any concrete implementation must be substitutable for its interface.

# If URLService works with AbstractURLRepository,
# it must work identically with ANY implementation.

service = URLService(url_repo=InMemoryURLRepository())   # works
service = URLService(url_repo=PostgresURLRepository())   # must also work
service = URLService(url_repo=RedisURLRepository())      # must also work

# Violation: PostgresURLRepository.get_url() raises NotImplementedError
# Violation: RedisURLRepository.create_url() has a different return type

Test for LSP: Write your service tests against the interface. If swapping the concrete implementation breaks a test, you have an LSP violation.

I — Interface Segregation Principle#

Do not force classes to implement methods they do not need.

# BAD: one fat interface
class AbstractRepository(ABC):
    def create(self): ...
    def get(self): ...
    def get_analytics(self): ...  # URLRepo doesn't need this
    def record_click(self): ...   # only ClickRepo should have this

# GOOD: separate interfaces by consumer
class AbstractURLRepository(ABC):
    def create_url(self): ...
    def get_url(self): ...

class AbstractClickRepository(ABC):
    def record_click(self): ...
    def get_clicks(self): ...

When to split: Split an interface when different consumers only use a subset of its methods. If URLService never calls record_click, it should not depend on an interface that has it.

D — Dependency Inversion Principle#

High-level modules depend on abstractions, not concrete classes.

# BAD: service depends on concrete class
class URLService:
    def __init__(self):
        self._repo = InMemoryURLRepository()  # coupled forever, untestable

# GOOD: service depends on interface, concrete class injected from outside
class URLService:
    def __init__(self, repo: AbstractURLRepository):
        self._repo = repo  # injectable, testable, swappable

# main.py is the ONLY place that knows about InMemoryURLRepository
service = URLService(repo=InMemoryURLRepository())

The payoff: DIP is what makes unit testing possible. When URLService takes AbstractURLRepository, you can inject a MockRepository in tests and verify service logic in total isolation from storage.


Design Patterns You Will Actually Use#

Do not memorise 23 GoF patterns. Know these 6 deeply and know when to reach for each.

Repository Pattern#

Use when: you need to isolate storage from business logic.

  • Abstracts away whether data lives in memory, Postgres, Redis, or a file.
  • Service layer never writes SQL, Redis commands, or file I/O.
  • The test: "If I replace the DB tomorrow, what do I have to change?" Answer: only the repo.

Strategy Pattern#

Use when: you have multiple algorithms for the same operation and want to swap them.

class AbstractShortCodeStrategy(ABC):
    def generate(self, counter: int) -> str: ...

class Base62Strategy(AbstractShortCodeStrategy):
    def generate(self, counter): return to_base62(counter)

class HashStrategy(AbstractShortCodeStrategy):
    def generate(self, counter): return md5_truncated(counter)

# URLRepository uses whichever is injected — zero changes to repo logic
class URLRepository:
    def __init__(self, strategy: AbstractShortCodeStrategy): ...

What to say: "I'd extract the short URL generation into a Strategy so we can swap Base62 for hash-based without changing the repo."

Factory Pattern#

Use when: object creation is complex or the exact type varies at runtime.

class PieceFactory:
    @staticmethod
    def create(piece_type: PieceType, color: Color, pos: Position) -> Piece:
        mapping = {
            PieceType.ROOK:   Rook,
            PieceType.KNIGHT: Knight,
            PieceType.QUEEN:  Queen,
        }
        return mapping[piece_type](color, pos)

When to reach for it: constructor logic with if/elif branching on a type is the signal.

Observer Pattern#

Use when: one event should trigger multiple independent reactions.

class EventBus:
    def subscribe(self, event_type, handler): ...
    def publish(self, event): ...

# URLService publishes ClickRecorded
# AnalyticsService, WebhookService, MLPipelineService all subscribe
# They never directly reference each other
  • Click recorded → update analytics, send webhook, trigger ML pipeline.
  • Order placed → send email, deduct inventory, notify warehouse.

What to say: "Rather than URLService directly calling AnalyticsService, I'd publish a ClickRecorded event. This decouples analytics from the redirect path and keeps p99 latency low."

Template Method Pattern#

Use when: multiple classes share the same algorithm skeleton but differ in one or two steps.

class BaseNotificationService(ABC):
    def send(self, user, message):          # template method — fixed skeleton
        content   = self.format(message)   # step 1: varies by subclass
        recipient = self.resolve(user)     # step 2: varies by subclass
        self._deliver(recipient, content)  # step 3: varies by subclass

    @abstractmethod
    def format(self, message): ...

    @abstractmethod
    def resolve(self, user): ...

    @abstractmethod
    def _deliver(self, recipient, content): ...

Singleton (use carefully)#

Use when: exactly one shared instance must exist — connection pool, config object.

  • Overused: making services singletons when DI already controls instantiation.
  • Appropriate: a shared in-memory counter, a config loader, a DB connection pool.

Caution: Singletons make testing harder because they carry global state. In interviews, prefer showing DI over Singleton — it signals more sophistication.


How to Articulate Trade-offs#

This is the single biggest differentiator between mid and staff-level candidates.

The Trade-off Template#

"I chose X over Y because [reason]. The trade-off is [what we gave up]. In production I would [mitigate by doing Z]."

Trade-off Statements by Category#

Storage and data structures:

  • "I used a dict for O(1) lookup on the hot path. The trade-off is memory growth — in production I'd cap size with an LRU eviction policy."
  • "I'm indexing by both short URL and alias. Two indexes means double the write cost but O(1) on both read paths."

Consistency vs availability:

  • "A counter + lock gives strong consistency but becomes a bottleneck under high write throughput. At scale I'd use Redis INCR which is atomic without a Python-level lock."
  • "I'm doing last-write-wins here. If we need stronger guarantees, I'd add a version field and use compare-and-swap."

Coupling vs performance:

  • "Calling AnalyticsService inline gives consistency but adds latency to the redirect. I'd decouple with a Kafka event — redirect returns in <5ms, analytics processes asynchronously."
  • "I'm storing expiry in the application layer. Redis TTL handles it for free — trade-off is TTL is approximate, not exact to the second."

Simplicity vs extensibility:

  • "A single AbstractRepository is simpler to start. I'd split it when the interface exceeds 5–6 methods — ISP starts paying off at that size."
  • "I could use inheritance here, but composition avoids the fragile base class problem and keeps dependencies explicit."

Short URL generation:

ApproachProsCons
Counter + Base62No collisions, compact, simpleSequential = enumerable, distributed counter needs coordination
Hash-based (MD5/SHA truncated)Non-sequential, statelessCollision probability, needs retry logic
UUIDZero collision riskLong, not URL-friendly

Time-Boxing — The 45-Minute Plan#

This is the fix for not being able to apply everything in time. You don't have a time budget — here is one.

TimeActivity
0:00 – 5:00THINKING RITUAL. No code. List entities, use-cases, failure modes, constraints. Say them out loud.
5:00 – 10:00Write all models. Get entities + fields on paper. No logic.
10:00 – 12:00Write all exceptions. One class per failure mode. Takes 2 minutes.
12:00 – 17:00Write interfaces. Method signatures and return types only.
17:00 – 27:00Write repo implementations. Fill in storage logic.
27:00 – 40:00Write service layer. This is where complexity lives — budget the most time here.
40:00 – 45:00Write main(). Wire DI. Demo one happy path + one error path.

Key rule: If you are behind schedule, cut depth in repos (stub methods with pass) — never cut interfaces or the service layer. Repos are swappable. Services and interfaces are the architecture.

When You Run Out of Time — What to Say#

Interviewers expect you not to finish. What they evaluate is whether you know what you left out.

  • "I've left castling as a stub — I know it needs special pre-condition checks: neither king nor rook has moved, and no squares in between are under attack."
  • "The repo is in-memory. In production this would be Redis for board state with Postgres for move history."
  • "I haven't handled concurrent moves — optimistic concurrency on a version field on the game record would handle that."

Signal: Saying "I know what I left out and how I would complete it" scores higher than scrambling to write more code in the last 2 minutes.


The Collaborator Extraction Rule#

When a service method grows a complex sub-problem with its own internal logic, extract it into a collaborator class and inject it.

# Signal: you are writing a comment block inside a service method

class GameService:
    def make_move(self, ...):
        # --- validate move legality ---   <-- this block wants to be its own class
        if piece.type == PAWN:
            ...  # 40 lines of pawn logic
        if piece.type == ROOK:
            ...  # 20 lines of rook logic

The extraction rule: if the block has its own internal state, its own test cases, or more than ~20 lines of logic, it is a collaborator, not inline code.

# After extraction
class GameService:
    def __init__(self, ..., validator: MoveValidator):
        self._validator = validator   # injected, mockable

    def make_move(self, ...):
        if not self._validator.is_valid_move(game, piece, to_pos):
            raise InvalidMoveError(...)

One-Page Cheat Sheet#

The 6 Questions#

QuestionWhere the answer lives
"What does my data look like?"model_class.py — pure dataclasses, no logic
"What can go wrong?"exceptions.py — one class per failure mode
"What must every storage impl support?"interfaces.py — abstract repo contracts
"How is data actually stored?"repo_class.py — concrete implementations
"What does this system DO?"service_class.py — use-cases, business logic
"How do I wire everything?"main.py — composition root, DI, no logic

SOLID in One Line Each#

  • S: One class, one reason to change. Extract when a class has two jobs.
  • O: Add new behaviour without editing existing code. Replace if/elif on type with polymorphism.
  • L: Any concrete repo must be substitutable for its interface without breaking the service.
  • I: Do not put click analytics methods on the URL repo interface. Split interfaces by consumer.
  • D: Services take interfaces, not concrete classes. main() is the only place that knows about InMemory*.

Patterns in One Line Each#

  • Repository: Isolate storage. Service never writes SQL or Redis commands.
  • Strategy: Swap algorithms by injecting different implementations.
  • Factory: Complex object creation — use when constructor logic branches on type.
  • Observer: Decouple side-effects. Event → subscribers process independently.
  • Template Method: Shared algorithm skeleton, steps vary by subclass.

Staff Signal Phrases#

  • "Before I write any code, let me identify the entities, use-cases, and constraints."
  • "I'm depending on the interface here so this is testable and storage-swappable."
  • "I chose X over Y — the trade-off is Z. In production I'd mitigate by..."
  • "This is a side-effect. I'm keeping it in the service, not the repo, so repos stay pure persistence."
  • "I know what I haven't implemented — here is what it would take to complete it."

Structure beats knowledge under pressure. Run the ritual. Every time.

Related Posts