Compile Ready
All low level design problems
Low Level Design/Concepts & Foundations/ Concept

LLD Best Practices

A pragmatic checklist for machine-coding rounds — naming, immutability, error handling, testing, and time management.

7m readBest PracticesClean CodeTestingMachine Coding

Introduction

LLD Best Practices & Interview Playbook is a practical operating system for machine-coding and low-level design rounds. The goal is not to draw the most elaborate model. The goal is to clarify the problem, choose stable boundaries, code the core flow safely, and explain how the design changes when requirements evolve.

For Amazon, Atlassian, and Google style interviews, the strongest signal is disciplined execution under time pressure: ask sharp questions, model entities and relationships, define interfaces only where behavior varies, implement incrementally, dry-run with examples, and close with extensibility tradeoffs.

Learning Objectives

  • Run a repeatable step-by-step framework for machine-coding and LLD interviews.

  • Translate requirements into cohesive entities, relationships, interfaces, and extension points.

  • Apply composition, interface-oriented design, immutability, defensive copying, and collection encapsulation in Java.

  • Spot design smells such as god classes, broad responsibilities, leaky collections, vague names, and exception abuse.

  • Write testable code by separating domain logic, policies, infrastructure, and object construction.

  • Explain tradeoffs clearly during dry-runs and follow-up extensibility discussions.

Core Theory

The interview loop: clarify, model, code, verify, extend

Treat an LLD round as a short engineering project with checkpoints.

  • Clarify requirements: Confirm users, core flows, constraints, and out-of-scope items.
  • Identify entities and relationships: Find nouns, ownership, cardinality, and invariants.
  • Define interfaces: Add contracts for behavior that varies or depends on external systems.
  • Pick patterns deliberately: Use Strategy, Factory, State, Observer, or Repository only when they simplify a real variation.
  • Code incrementally: Build the smallest working vertical slice first.
  • Dry-run: Walk through one happy path and one edge case with object state changes.
  • Discuss extensibility: Explain which changes are additive and which require deeper refactoring.

Clarify requirements before naming classes

Good candidates avoid designing from assumptions. Spend the first few minutes turning an ambiguous prompt into a bounded problem.

Useful questions include:

  • What are the primary actors and top flows?
  • Is concurrency expected in this round or can we design single-threaded first?
  • Which operations must be supported now and which are future extensions?
  • What should happen for invalid input, duplicate actions, and unavailable resources?
  • Is persistence required or can storage be in memory?

Then state your scope back to the interviewer. This prevents overbuilding and gives you permission to defer non-core pieces.

Identify entities through responsibilities and relationships

Start with domain nouns, but do not stop there. A class is justified when it owns behavior or protects an invariant.

For each candidate entity, answer:

  • What state does it own?
  • Which behavior belongs next to that state?
  • What invariant would be broken if callers mutated it directly?
  • Does it have a one-to-one, one-to-many, or uses-a relationship with other entities?

This turns vague nouns into a coherent object graph. For example, a ParkingLot owns Levels, a Level owns ParkingSpots, and a PricingPolicy is used by the checkout flow rather than inherited by the lot.

Program to interfaces at variation points

An interface is valuable when the caller should depend on a behavior, not on one concrete implementation. It is not valuable when every class gets an interface by habit.

Use interfaces for policies, gateways, repositories, clocks, id generators, notification channels, payment methods, matching strategies, and pricing rules. Keep them small and named by capability.

public interface PricingPolicy {
    Money priceFor(Ticket ticket);
}

public final class HourlyPricingPolicy implements PricingPolicy {
    private final Money hourlyRate;

    public HourlyPricingPolicy(Money hourlyRate) {
        this.hourlyRate = hourlyRate;
    }

    @Override
    public Money priceFor(Ticket ticket) {
        return hourlyRate.multiply(ticket.hoursRoundedUp());
    }
}

public final class CheckoutService {
    private final PricingPolicy pricingPolicy;

    public CheckoutService(PricingPolicy pricingPolicy) {
        this.pricingPolicy = pricingPolicy;
    }

    public Receipt checkout(Ticket ticket) {
        Money amount = pricingPolicy.priceFor(ticket);
        return new Receipt(ticket.id(), amount);
    }
}

Favor composition over inheritance

Composition keeps behavior swappable and local. Inheritance should represent stable subtype identity, not just shared fields or convenience reuse.

Use the sentence test. If the relationship is naturally is-a and every child can replace the parent safely, inheritance may fit. If the relationship is has-a, uses-a, configured-by, or delegates-to, composition is usually cleaner.

public final class RideMatcher {
    private final MatchingStrategy matchingStrategy;
    private final DriverRepository drivers;

    public RideMatcher(MatchingStrategy matchingStrategy, DriverRepository drivers) {
        this.matchingStrategy = matchingStrategy;
        this.drivers = drivers;
    }

    public Driver match(RideRequest request) {
        List<Driver> nearbyDrivers = drivers.nearby(request.pickup());
        return matchingStrategy.bestDriver(request, nearbyDrivers);
    }
}

public interface MatchingStrategy {
    Driver bestDriver(RideRequest request, List<Driver> drivers);
}

Use immutability and defensive copying for safe boundaries

Mutable objects are harder to reason about during a dry-run. Prefer immutable value objects for identifiers, money, coordinates, time ranges, and request snapshots.

When a class receives or returns collections, make defensive copies. This prevents outside code from changing internal state after validation has already passed.

public final class Order {
    private final OrderId id;
    private final List<OrderLine> lines;

    public Order(OrderId id, List<OrderLine> lines) {
        if (lines == null || lines.isEmpty()) {
            throw new IllegalArgumentException("order must have at least one line");
        }
        this.id = id;
        this.lines = List.copyOf(lines);
    }

    public OrderId id() {
        return id;
    }

    public List<OrderLine> lines() {
        return List.copyOf(lines);
    }
}

Encapsulate collections instead of exposing mutation

A collection is often where domain invariants live. If callers can freely add, remove, or reorder items, the owning class no longer controls its rules.

Expose intention-revealing methods such as addItem, reserveSpot, assignDriver, or cancelBooking. Return read-only snapshots when callers need to inspect state.

public final class Cart {
    private final Map<ProductId, CartLine> lines = new HashMap<>();

    public void addItem(Product product, int quantity) {
        if (quantity <= 0) {
            throw new IllegalArgumentException("quantity must be positive");
        }
        lines.merge(product.id(), new CartLine(product, quantity), CartLine::merge);
    }

    public void removeItem(ProductId productId) {
        lines.remove(productId);
    }

    public List<CartLine> items() {
        return List.copyOf(lines.values());
    }
}

Keep classes SRP-sized and avoid god classes

A god class performs too many unrelated tasks: validation, pricing, persistence, notification, object creation, and orchestration. It may look fast to write, but it becomes hard to test and harder to extend during follow-ups.

Use the reason-to-change test. If two parts of a class change for different business reasons, split them into collaborators. A good LLD class usually has one main responsibility, a small set of fields, and methods that share the same vocabulary.

Use meaningful names that reveal the domain

Names are part of the design. Prefer names that explain a role or policy, not the implementation detail you happened to choose first.

Strong names: TicketIssuer, PricingPolicy, SpotAllocationStrategy, PaymentGateway, BookingRepository, NotificationSender.

Weak names: Manager, Handler, Processor, Data, Util, or Helper when they hide multiple responsibilities. If you use a broad word, qualify it with the domain role.

Design exceptions as part of the contract

Exception handling should tell callers what went wrong and whether they can recover. Do not swallow exceptions, return null for failed operations, or throw one generic runtime exception for every case.

Use validation exceptions for bad caller input, domain exceptions for rule violations, and adapter exceptions for infrastructure failures. In interviews, keep the hierarchy small and explain which layer catches what.

public final class BookingService {
    private final ShowRepository shows;

    public BookingService(ShowRepository shows) {
        this.shows = shows;
    }

    public Booking reserve(ShowId showId, SeatId seatId) {
        Show show = shows.findById(showId)
            .orElseThrow(() -> new NotFoundException("show not found"));
        if (!show.isSeatAvailable(seatId)) {
            throw new SeatUnavailableException(seatId);
        }
        return show.reserve(seatId);
    }
}

Write testable code from the first class

Testability is a design outcome. If the core flow can be tested without a database, clock, random generator, network call, or console input, your boundaries are probably healthy.

Prefer constructor injection, pure domain methods, small interfaces for external dependencies, and deterministic value objects. Avoid static global state in the core model unless the interviewer explicitly asks for a singleton-like constraint.

public final class TokenService {
    private final Clock clock;
    private final IdGenerator ids;

    public TokenService(Clock clock, IdGenerator ids) {
        this.clock = clock;
        this.ids = ids;
    }

    public Token issue(UserId userId) {
        Instant expiresAt = clock.instant().plusSeconds(900);
        return new Token(ids.nextId(), userId, expiresAt);
    }
}

Diagrams

LLD interview workflow

Rendering diagram…
A strong interview answer is iterative. Clarification can change the entity model, the dry-run can reveal a missing invariant, and extensibility discussion closes the loop.

Comparisons

Good design signals vs bad design smells

AreaGood signalBad smellInterview fix
RequirementsScope is stated with assumptions and exclusionsCandidate starts coding before clarifying flowsAsk questions, restate scope, and prioritize the core scenario
EntitiesClasses own behavior and protect invariantsClasses are passive data bags with public mutationMove validation and state transitions into the owning class
RelationshipsComposition models ownership and collaborationInheritance is used only to reuse fieldsApply the is-a test and prefer has-a composition otherwise
InterfacesContracts appear at true variation pointsEvery class has a matching interface by defaultIntroduce small interfaces for policies, gateways, and repositories
State safetyValue objects are immutable and collections are copiedMutable lists or maps leak from gettersUse constructors, defensive copying, and read-only snapshots
Class sizeEach class has one clear reason to changeOne manager class validates, stores, prices, and notifiesSplit domain rules, orchestration, infrastructure, and policies
ErrorsExceptions communicate input, domain, or infrastructure failureNulls, booleans, or generic errors hide failure meaningUse a small exception vocabulary and document caller behavior
TestingCore logic runs with fake collaboratorsLogic depends directly on time, network, console, or databaseInject clocks, repositories, gateways, and id generators

Machine-coding checklist

CheckpointWhat to sayWhat to buildTimebox
ClarifyI will support these flows and defer these extensionsRequirement notes and edge cases3 to 5 minutes
ModelThese entities own these invariantsClass skeletons and relationships5 to 8 minutes
ContractsThis behavior varies, so callers depend on an interfaceSmall policies or gateways3 to 5 minutes
Core implementationI will make one vertical flow work firstDomain methods and service orchestration15 to 20 minutes
Dry-runLet us walk through state changes for a sample inputOne happy path and one edge case5 minutes
ExtensionThis change is additive because the workflow depends on a contractNew policy, adapter, or repository implementation3 to 5 minutes

Best Practices

  • Clarify the core flows, actors, constraints, and out-of-scope features before designing classes.

  • Start with entities that own behavior and invariants, not with database tables or field lists.

  • Favor composition over inheritance unless the subtype is safely substitutable for the parent.

  • Program to interfaces for policies and external dependencies that may vary.

  • Keep interfaces small and named by capability, such as PaymentGateway or AllocationStrategy.

  • Use immutable value objects for money, ids, timestamps, coordinates, and request snapshots.

  • Make defensive copies of collections in constructors and getters.

  • Encapsulate collections behind intention-revealing methods instead of exposing mutable lists or maps.

  • Keep classes SRP-sized with one primary reason to change.

  • Use meaningful domain names and avoid vague suffixes unless the role is clear.

  • Design a small exception vocabulary that separates validation, domain, and infrastructure failures.

  • Write testable code with constructor injection and deterministic collaborators.

  • Implement incrementally and keep a runnable happy path throughout the interview.

  • Dry-run with concrete objects and state changes instead of only describing abstract flows.

  • Close with extensibility points and tradeoffs rather than claiming the design handles everything.

Common Mistakes

  • ×

    Skipping requirement clarification and discovering major scope differences after most code is written.

  • ×

    Creating a god class that validates, stores, prices, notifies, and coordinates every flow.

  • ×

    Using inheritance for shared fields when composition would model the relationship better.

  • ×

    Adding interfaces for every class, creating ceremony without real variation.

  • ×

    Returning mutable internal collections and letting callers bypass invariants.

  • ×

    Using static utilities for core domain rules, making tests and extensions harder.

  • ×

    Naming every orchestration class Manager or Processor without a precise responsibility.

  • ×

    Throwing generic exceptions or returning null for meaningful domain failures.

  • ×

    Optimizing for patterns before the simple object model is correct.

  • ×

    Writing a complete model without a dry-run, then missing state transitions or edge cases.

Quiz

0/7 answered

  1. 1.What should you do first in a machine-coding or LLD round?

  2. 2.When is composition usually better than inheritance?

  3. 3.What is the best reason to introduce an interface in an LLD answer?

  4. 4.Why should a getter avoid returning a mutable internal list?

  5. 5.Which class is most likely violating SRP?

  6. 6.What makes code testable in an LLD interview?

  7. 7.What is the best way to dry-run an LLD solution?

Flashcards

Cheat Sheet

Interview framework: Clarify requirements, identify entities and relationships, define interfaces, pick patterns, code incrementally, dry-run, and discuss extensibility.

Clarification: Confirm actors, core flows, constraints, storage expectations, concurrency expectations, invalid input behavior, and out-of-scope features.

Entity design: A class should own behavior and invariants, not just fields. Describe what each class protects and why it exists.

Composition: Prefer has-a and uses-a relationships for policies, services, repositories, and owned parts. Use inheritance only for true is-a subtype identity.

Interfaces: Add contracts at variation points. Keep them small, capability-named, and easy to fake in tests.

State safety: Prefer immutable value objects. Use defensive copies for collections and expose intention-revealing methods instead of mutable getters.

Class size: Avoid god classes. Split responsibilities by reason to change: domain rules, orchestration, persistence, external calls, and notifications.

Naming: Choose domain names that reveal responsibility. Avoid vague Manager, Handler, Processor, Util, and Data names unless qualified clearly.

Exceptions: Separate validation failures, domain rule violations, and infrastructure failures. Do not hide errors behind nulls or generic booleans.

Testing: Use constructor injection for clocks, id generators, repositories, and gateways. Keep core domain logic deterministic.

Close strong: Dry-run concrete inputs, mention edge cases, and explain exactly where a new requirement can be added without editing the core workflow.

References

  • BookEffective JavaJoshua Bloch
  • BookClean CodeRobert C. Martin
  • BookRefactoring: Improving the Design of Existing CodeMartin Fowler
  • BlogGoogle Testing Blog