Skip to content
QSWEQB

Low-level design fundamentals

What encapsulation is actually protecting, each SOLID letter stated as a consequence rather than a slogan, the patterns worth naming and the ones worth refusing, and how a live parking-lot question is scored. Sixty items, sixteen worked through with code, a table or a diagram.

60 questions

Go deeper on Low-Level Design & OOD

Objects and responsibilities

What is encapsulation actually protecting?

Not the fields — the invariants that relate them. A class with private fields and a getter and setter for each one is exactly as exposed as a struct, because any caller can still put it into a state it should never occupy. Encapsulation means the object owns the rules about what combinations of values are legal, so an Order refuses to add a line once it is dispatched rather than trusting every call site to check first. That reframing is what makes the follow-up answerable: the question is never "are the fields private" but "which statements about this object are always true, and what code could break one".

Why is composition preferred over inheritance?

Because inheritance couples you to the superclass's implementation, not just its interface, and that coupling is permanent and invisible from the subclass's own file. Composition gives you the same reuse through delegation while leaving the relationship explicit and replaceable at runtime, which is also what makes the collaborator injectable in a test. The rule of thumb is that inheritance models "is a permanent kind of" and composition models "has a" or "uses a", and most things candidates model as inheritance are really varying behaviour — which is a strategy held by reference, not a subclass. The cost of composition is more wiring and one more indirection to read through.

What is the fragile base class problem?

A superclass cannot be changed safely once subclasses exist, because subclasses may depend on which of its own methods it calls internally. If Collection.addAll happens to loop over add, a subclass that overrode both to count elements double-counts, and the bug appears when the base class is refactored rather than when the subclass is written. Nothing in the compiler or the tests of either class catches it, since each is correct in isolation. The mitigations are to document self-calls as part of the contract, to make classes final unless designed for extension, or to avoid the inheritance and delegate instead.

Show me a god class refactored into responsibilities.

The class works and the tests pass. The problem is that every change touches it.

Before: OrderService, 1,400 lines, 31 public methods

  validates the cart          -> business rules
  applies discount codes      -> pricing rules
  calls the payment gateway   -> external integration
  writes rows to three tables -> persistence
  renders the receipt email   -> presentation
  publishes an audit event    -> messaging
  formats currency for the UI -> formatting

Reasons this class changes: a tax rate, a gateway API version, a schema
migration, a copy edit in the email, a new audit field. Five unrelated teams,
one file, one merge conflict.

The useful test is not size, it is the number of reasons to change. Every distinct actor who can force an edit is a separate responsibility, and a class with five of them will be edited five times as often as anything else in the codebase, which is where the merge conflicts and the accidental regressions come from.

The refactoring is mechanical once you have that list. PricingPolicy owns discounts and tax. PaymentGateway is an interface with one adapter per provider. OrderRepository owns persistence. ReceiptMailer owns the email. OrderService keeps only the orchestration — the sequence of steps, the transaction boundary, and what happens when a step fails.

The detail candidates miss is what the orchestrator is allowed to keep. Splitting a god class into six anaemic data holders plus a service that still does all the thinking has moved the problem, not solved it: the behaviour belongs with the data it constrains, so the discount calculation goes into PricingPolicy with the rates, not into a static helper the service calls.

The measurable win is testability. Pricing rules become a pure unit test with no gateway, no database and no mail server, and that is usually the moment the team discovers which of the rules nobody could previously state.

What is cohesion, and how do you tell a class has too little?

Cohesion is how much the members of a class belong together — whether the methods operate on the same state toward the same purpose. The practical signal of low cohesion is that different methods touch disjoint subsets of the fields, so you can draw a line through the class and separate it without breaking anything. A second signal is the name: if the only honest name is Manager, Helper, Utils or Processor, the class has no single concept, because you were unable to say what it is. Low cohesion matters because it drags unrelated reasons to change into one file.

What does "tell, don't ask" mean in practice?

Send an object a message describing intent rather than extracting its state, deciding elsewhere, and pushing state back. account.withdraw(amount) keeps the overdraft rule inside the account; if (account.getBalance() >= amount) account.setBalance(account.getBalance() - amount) copies that rule to every call site and guarantees one of them will disagree. The deeper point is that getters-plus-external-logic is how behaviour leaks out of a class and how the same validation ends up implemented three times slightly differently. It is not absolute — reporting and serialisation legitimately need to read state — but a class whose entire surface is accessors has no behaviour to protect.

What is the law of Demeter, and when is it worth breaking?

A method should only talk to its own fields, its parameters, and objects it created — not to things reached by navigating through them. The symptom is a chain like order.getCustomer().getAddress().getCountry().getTaxRate(), which couples the caller to four classes and breaks when any link changes shape or returns null. The fix is usually to move the behaviour rather than the navigation, asking order.taxRate() instead. It is worth breaking for fluent builders and query DSLs, where each call returns the same conceptual object rather than reaching into a stranger's internals, and for plain data structures that have no invariants to protect.

SOLID in practice

What does the single responsibility principle actually mean?

That a class should have one reason to change, which is a statement about who can force an edit rather than about how many methods it has. The version people recite — "a class should do one thing" — is useless because "one thing" is whatever granularity you choose; the actor-based version is testable, because you can list the roles that can demand a change and count them. A class serving the compliance team, the pricing team and the design system has three, so any of them can break the other two, and the file will be permanently contended. The cost of over-applying it is a spray of one-method classes with no obvious home.

Show me each SOLID letter as a consequence rather than a slogan.

Every letter names a specific failure it prevents, and the failure is the part worth being able to state.

letter  restated as a consequence                     failure it prevents
------  -------------------------------------------   ---------------------------
S       one actor can force a change to this file     unrelated teams editing the
                                                      same class and conflicting
O       new cases arrive as new classes, not as new    every new payment type
        branches in an existing switch                reopens and re-tests old code
L       a subtype can be substituted without the      a caller special-cases on
        caller knowing which one it received          type, so polymorphism is fake
I       a client depends only on the methods it        implementing five methods
        calls                                         that throw to satisfy a type
D       both sides depend on an interface owned by     the domain cannot be tested
        the caller's layer                            without a live database

Read the right-hand column as the actual definition. Each letter exists because a concrete, recurring bug exists, and naming that bug is what distinguishes understanding the principle from having memorised the acronym.

Two of them are commonly misread. Open/closed does not mean never editing a file; it means the extension point is designed, so adding a case does not require understanding the cases already there. And dependency inversion is not "use interfaces everywhere" — it is specifically that the interface is defined by and owned by the higher-level module, so a repository interface lives with the domain and its SQL implementation lives in the infrastructure layer.

The honest caveat to offer is that all five are heuristics with a cost. Applied without judgement they produce an interface per class, a factory per interface, and indirection nobody can follow. The senior version of the answer names the bug each one prevents and says you apply it when that bug is plausible.

Show me the Liskov substitution principle violated concretely.

The square-rectangle case is worth walking because the model is mathematically correct and still wrong.

class Rectangle {
    protected int width, height;
    void setWidth(int w)  { this.width = w; }
    void setHeight(int h) { this.height = h; }
    int area() { return width * height; }
}

class Square extends Rectangle {          // a square IS a rectangle, surely
    @Override void setWidth(int w)  { this.width = w; this.height = w; }
    @Override void setHeight(int h) { this.width = h; this.height = h; }
}

// A caller that knows nothing about squares:
void resize(Rectangle r) {
    r.setWidth(5);
    r.setHeight(4);
    assert r.area() == 20;                // fails when r is a Square: area is 16
}

The subclass broke a rule the caller was entitled to rely on — that width and height vary independently. Nothing about geometry is wrong here; the inheritance is wrong, because Rectangle's mutable interface is not something a square can honour.

The general form of the violation is worth naming, because it appears far from geometry. A subtype must not strengthen preconditions, weaken postconditions, or throw exceptions the base type's contract does not permit. A ReadOnlyList extending List and throwing on add is the same bug: every caller holding a List believes it can add.

The tell in real code is a caller doing if (x instanceof Y) or checking a isReadOnly() flag before deciding what it may call. That check is the caller compensating for a broken substitution, and it will be copied to every other call site over time.

Two fixes. Make the type immutable, so withWidth returns a new instance and a square can safely be a rectangle. Or drop the inheritance: both implement a Shape interface exposing only area(), which is the behaviour they genuinely share.

What does the open/closed principle actually cost?

An extension point you had to predict. Making a class open for extension means choosing an axis of variation in advance — payment method, tax jurisdiction, export format — and every axis you guess wrong is an abstraction that costs indirection and buys nothing. So the disciplined version is to write the switch statement the first time, and only introduce the interface when the second or third case arrives and shows you where the variation actually is. Applying the principle speculatively produces the strategy hierarchy with one implementation, which every reviewer has seen and nobody can delete safely.

Show me an interface segregated from a fat one.

The fat interface is usually discovered by the implementation that cannot honour it.

// Fat: every implementer must support everything.
interface Printer {
    void print(Doc d);
    void scan(Doc d);
    void fax(Doc d);
    void staple(Doc d);
}

class BasicPrinter implements Printer {
    public void print(Doc d) { /* real */ }
    public void scan(Doc d)  { throw new UnsupportedOperationException(); }
    public void fax(Doc d)   { throw new UnsupportedOperationException(); }
    public void staple(Doc d){ throw new UnsupportedOperationException(); }
}
// Segregated: capabilities are separate types, composed where present.
interface Printer { void print(Doc d); }
interface Scanner { void scan(Doc d);  }
interface Fax     { void fax(Doc d);   }

class BasicPrinter implements Printer { public void print(Doc d) { } }
class OfficeMfd implements Printer, Scanner, Fax { /* all three */ }

The three UnsupportedOperationException bodies are the diagnosis. They are also a Liskov violation, so the two principles catch the same defect from different directions: the caller holds a Printer and cannot call half of it without knowing the concrete type.

The client-side benefit is the one that matters more and gets mentioned less. A function that only prints now declares Printer and can be tested with a four-line fake, instead of a mock stubbing four methods it never invokes. The size of your test doubles is a reliable read on how segregated your interfaces are.

The cost is more types, and the failure mode of over-applying it is an interface per method, which makes the composed classes noisy. The natural grain is the capability a client actually depends on — if no caller ever wants scanning without printing, one interface is correct.

What does dependency inversion buy you, beyond testability?

A stable direction for your dependency graph. When the domain owns the interface and the infrastructure implements it, the domain compiles and reasons without knowing that a database, a broker or an HTTP client exists, so those choices can change without a domain edit. Testability is a symptom of that, not the goal. The consequence people miss is who owns the interface: putting OrderRepository in the persistence package alongside its implementation inverts nothing, because the arrow still points from the domain into infrastructure. The interface must live with its caller for the inversion to have happened at all.

What is the difference between dependency inversion and dependency injection?

Inversion is the design rule about which way the dependency arrow points, and it is satisfied by where an interface is declared. Injection is the mechanical technique of supplying a collaborator from outside rather than constructing it inside, and it can be done with or without any inversion at all — a constructor taking a concrete PostgresOrderRepository is injection with no inversion. Conflating them produces the belief that adding a framework container makes a design decoupled, when a container full of concrete types is just a slower new. Constructor injection is the form worth defaulting to, since it makes a missing dependency a compile error.

Which SOLID letter do candidates misapply most?

Single responsibility, in the direction of too much. "One thing" gets read at method granularity, so a class is split until each holds one function, and the result is thirty files where the sequence of steps is no longer readable anywhere — the coupling did not go away, it became invisible. The second most misapplied is dependency inversion, producing an interface with exactly one implementation for every class in the system on the grounds that it might be swapped. Both failures share a cause: applying the rule without naming the bug it prevents, so there is no signal for when to stop.

Creational patterns

When does a factory earn its place?

When choosing which concrete type to create is itself logic that would otherwise be duplicated, or when the caller must not know the concrete types at all. A factory reading a country code and returning the right TaxCalculator earns it, because that mapping exists once and the caller stays ignorant. A factory whose body is a single new does not: it is a constructor with extra ceremony and one more file to open when reading the code. The other genuine case is when construction requires work the constructor should not do — a lookup, validation across several inputs, or returning a cached instance.

What is the difference between a factory method and an abstract factory?

A factory method is one overridable method that decides which single product to create, so subclasses vary one type. An abstract factory is an object with several such methods that produce a family of products guaranteed to be compatible — a widget factory returning a matching button, scrollbar and menu. The distinction matters because the abstract factory is only justified when the compatibility constraint is real; without it you have paid for a second layer of indirection to do what one factory method does. Its known weakness is that adding a product type to the family changes the interface and therefore every implementation.

Show me a builder replacing a six-argument constructor.

Six parameters, four of them optional, most of them the same type.

// The call site nobody can read or verify.
new Report(
    "Q3 revenue",      // title
    "revenue",         // dataset
    true,              // includeCharts?
    false,             // includeRawData?
    true,              // landscape?
    null               // watermark, meaning none
);

// And the constructor telescope it came from:
Report(String t, String d) { this(t, d, false, false, false, null); }
Report(String t, String d, boolean c) { this(t, d, c, false, false, null); }
// ...four more overloads, and two booleans that can be swapped silently
Report r = Report.builder("Q3 revenue", "revenue")   // required args here
        .withCharts()
        .landscape()
        .build();                                     // validates, then constructs

The bug the builder removes is not verbosity, it is that adjacent booleans of the same type are interchangeable to the compiler. Swap includeCharts and includeRawData and everything compiles, the report is wrong, and the diff looks innocuous.

Two design details do the real work. The required arguments belong on the builder's entry point rather than as optional setters, so a mandatory field cannot be forgotten. And build() is where cross-field validation lives — a watermark with no charts, landscape with a portrait-only dataset — because that is the one place all the values exist at once and the only place a half-built object cannot escape from.

The cost is a second class to maintain in step with the first, which is why the threshold matters. At three parameters a constructor is clearer. The builder earns its place when parameters are numerous, mostly optional, or type-ambiguous. Records and named arguments remove much of the need in languages that have them.

Show me a singleton making a test impossible.

The class works in production and the test cannot be written.

class Clock {
    private static final Clock INSTANCE = new Clock();
    static Clock get() { return INSTANCE; }
    Instant now() { return Instant.now(); }
}

class SubscriptionService {
    boolean isExpired(Subscription s) {
        return Clock.get().now().isAfter(s.expiresAt());   // hidden dependency
    }
}

// The test you want to write:
//   given a subscription expiring on 1 March, when the date is 2 March,
//   then it is expired.
// There is no seam. You cannot set the time. The only options are to wait,
// to fabricate dates relative to now, or to reach for a bytecode-level
// static mock — and the third one hides the design problem permanently.
class SubscriptionService {
    private final Clock clock;
    SubscriptionService(Clock clock) { this.clock = clock; }   // the seam
    boolean isExpired(Subscription s) {
        return clock.now().isAfter(s.expiresAt());
    }
}
// Test: new SubscriptionService(fixedClock("2026-03-02"))

The injected version is not merely more testable, it is more honest: the dependency on time is now visible in the constructor signature, so a reader can see what this class needs. A static call graph hides dependencies, which is why a codebase with many singletons is hard to reason about long before it is hard to test.

The second problem the test exposes is shared state across tests. A singleton holding mutable state carries it from one test to the next, producing failures that depend on execution order and vanish when the failing test is run alone.

The refactor path in existing code is to keep Clock.get() as a deprecated convenience that delegates, inject the dependency in the classes you are already touching, and let the static call sites drain over time rather than in one commit.

Why is the singleton pattern mostly an antipattern?

Because it combines two decisions that should be separate: that only one instance should exist, and that everyone may reach it globally. The second is the harmful one — it is global mutable state, so dependencies become invisible, tests share contamination and ordering becomes significant, and concurrent access to the shared mutable field is a race nobody declared. Wanting one instance is often legitimate; the correct expression is to construct one at the composition root and inject it, which keeps the cardinality and drops the global reach. The defensible uses are genuinely stateless, immutable objects such as a null implementation or a lookup table.

When is a static factory method better than a constructor?

When the name carries information a constructor cannot, when you want to return a cached or shared instance, or when you want to return a subtype. Duration.ofDays(2) and Duration.ofSeconds(2) are distinguishable in a way that two Duration(long) constructors are not, and Optional.empty() can return the same instance every time. The cost is that a static factory is not inherited and does not participate in subclass construction, so a class designed for extension still needs a constructor. It is also invisible in some tooling that lists constructors, which matters less than the naming benefit.

What is the prototype pattern for, and where does it go wrong?

Creating a new object by copying an existing configured one, which is useful when construction is expensive or when the configuration was assembled at runtime and you want variants of it. Where it goes wrong is depth: a shallow copy shares every mutable referenced object, so mutating the clone's list mutates the original's, and that bug appears far from the clone call. A deep copy is correct and must be maintained as fields are added, which is a maintenance obligation nothing in the compiler enforces. Preferring immutable objects removes the need entirely, which is usually the better answer.

Structural patterns

Show me how to tell adapter, facade, decorator and proxy apart.

All four wrap an object, which is why they blur. The distinguishing question is what changes across the wrapper boundary.

pattern     interface in vs out       what it adds            typical trigger
----------  ------------------------  ----------------------  -------------------
adapter     different: converts A     nothing, only shape     a third-party API
            to the interface B                                does not fit yours
            that your code expects

facade      different: one simple     nothing, only           a subsystem of six
            interface over many       simplification          classes and an order
                                                              of calls to get right

decorator   same interface in         behaviour, stackable    add caching, retries,
            and out                                           logging to one method

proxy       same interface in         access control,         lazy load, remote
            and out                   not behaviour           call, permission check

The clean test is two questions. Does the wrapper expose the same interface as what it wraps? If no, it is an adapter or a facade — an adapter when the target interface already exists and you are converting to it, a facade when you invented the simpler interface yourself. If yes, it is a decorator or a proxy.

Separating those two is where candidates hesitate, and the answer is intent rather than structure, because the code can look identical. A decorator adds behaviour the caller wanted more of and is designed to stack — retrying(caching(client)). A proxy controls whether and when the real call happens at all, and there is normally exactly one of it.

The reason the distinction earns marks is that it predicts different problems. A decorator chain's ordering is significant and easy to get wrong: caching outside retrying caches the failure, retrying outside caching retries the cache lookup. A proxy's problem is that it lies about cost — a lazily-loading proxy makes a field access issue a query, which is exactly the N+1 problem an ORM produces.

What is an adapter really for?

Keeping a foreign interface out of your code. You define the interface your domain wants, and the adapter is the single class that knows the vendor's method names, error semantics and data shapes, so a vendor change or a vendor swap is one file. The corollary is the one that matters in review: an adapter must also translate failures, converting the vendor's exception types into yours, otherwise the vendor's SdkTimeoutException propagates through your domain and the abstraction was decorative. It is also the seam that makes the integration testable without the third party.

What does a decorator cost?

Depth in the stack trace and ambiguity about ordering. Each decorator is another frame between the caller and the work, so a five-deep chain produces traces nobody can read and makes stepping through in a debugger tedious. More seriously, the composition order changes behaviour and nothing declares the correct order, so a later maintainer wiring the chain differently introduces a subtle defect — caching an error response, or authorising after logging the unauthorised attempt. The mitigation is to assemble the chain in exactly one place, with a comment saying why the order is what it is.

What is a proxy actually used for?

Controlling access to an object without the caller knowing. The three common purposes are laziness, where the real object or its data is loaded only on first use; remoteness, where the proxy turns a method call into a network call; and protection, where it checks permissions before delegating. Frameworks generate them constantly — transaction management, security annotations and lazy ORM associations are all proxies. That is worth knowing because it explains two recurring surprises: a self-call inside a class bypasses the proxy and therefore the transaction, and a field access can silently trigger a query.

When is a facade the wrong answer?

When it becomes the only permitted route to the subsystem and grows a method for every caller's needs. A facade is a convenience over a subsystem that remains usable directly; once it is mandatory, every new requirement widens it, and you have a god class whose name happens to end in Facade. The second failure is a facade that leaks — returning the subsystem's own types, so callers depend on the internals anyway and the simplification is illusory. A facade earns its keep when there is a correct sequence of calls or a set of defaults worth encoding once.

What is the composite pattern good for?

Treating a single item and a group of items through the same interface, so clients recurse without special cases — the canonical examples being filesystem trees, UI component hierarchies and nested pricing rules. The gain is that total() on a leaf and on a container are the same call, so client code has no branching on which it holds. The costs are two. The interface tends to grow methods that only make sense on containers, such as add, which leaves leaves throwing on them, and unbounded recursion over a deep or cyclic structure is a stack overflow waiting for production data.

What is the flyweight pattern, and does it still matter?

Sharing immutable instances so that many logical objects reference few real ones, splitting state into intrinsic — shared, in the flyweight — and extrinsic — passed in per use. It matters less than it did for object count, since memory is cheaper, but the idea is alive under other names: string interning, boxed integer caches, and a single shared Currency or Locale instance rather than millions of copies. The precondition is genuine immutability, because a shared instance that someone mutates corrupts every user of it, and that bug is nearly impossible to attribute to its cause.

What problem does the bridge pattern solve?

Two dimensions of variation multiplying into a class explosion. If you have three shapes and three renderers and model it with inheritance, you need nine classes and a tenth on either new axis. The bridge separates the abstraction hierarchy from the implementation hierarchy and connects them by composition, so three plus three classes cover it and each axis grows independently. In practice it is composition applied deliberately at a known second axis, which is why the pattern is more useful as a diagnostic — if adding a variant means editing several parallel hierarchies, you have two dimensions and one inheritance tree.

Behavioural patterns

Show me strategy replacing a switch statement.

The switch is fine until the third case, at which point every addition reopens a tested file.

BigDecimal fee(Payment p) {
    switch (p.method()) {
        case CARD:     return p.amount().multiply(new BigDecimal("0.029"))
                                        .add(new BigDecimal("0.30"));
        case BANK:     return new BigDecimal("0.25");
        case WALLET:   return p.amount().multiply(new BigDecimal("0.019"));
        default: throw new IllegalArgumentException(p.method().toString());
    }
}
// And the same switch appears in settlement, in refunds, and in reporting,
// each with a slightly different set of cases handled.
interface FeePolicy { BigDecimal fee(Money amount); }

final class CardFee   implements FeePolicy { /* 2.9% + 30p */ }
final class BankFee   implements FeePolicy { /* flat 25p    */ }
final class WalletFee implements FeePolicy { /* 1.9%        */ }

// One place maps method to policy; everything else takes the policy.
BigDecimal fee(Payment p) { return policies.get(p.method()).fee(p.amount()); }

The real defect in the first version is not the switch, it is that the switch is duplicated. Adding a payment method means finding every one of them, and the compiler helps only if the language checks exhaustiveness. Strategy collapses that to one registry, so a missing method fails at wiring time rather than in production on an unusual code path.

Two things to say before being asked. Strategy does not remove the conditional, it relocates it to the single point where the concrete type is chosen — usually a factory or a map — and pretending otherwise invites the follow-up. And a strategy with one implementation is worse than a method, so the trigger is the second or third case, not the first.

The alternative worth naming is an enum with behaviour, covered below. When the set of cases is closed and small, putting the fee calculation on the enum constant gives you the same dispatch with less ceremony and exhaustiveness for free.

What is the difference between strategy and template method?

Both vary part of an algorithm; they differ in mechanism and in what is fixed. Template method puts the invariant sequence in a base class and lets subclasses override named steps, so the skeleton is fixed at compile time and the variation arrives by inheritance. Strategy holds the varying behaviour as an injected object, so it can change at runtime and be tested alone. Prefer strategy by default, because template method inherits every fragile-base-class problem and binds one subclass to one variation. Template method is reasonable when the steps are genuinely meaningless outside the sequence and the sequence must not vary.

Show me observer leaking a listener, and the fix.

Nothing throws. The application simply grows and eventually dies.

sequenceDiagram
    participant V as View
    participant S as Subject
    participant G as Collector
    V->>S: addListener this
    S-->>V: notify on change
    V->>V: user navigates away, view discarded
    G--xV: cannot collect, subject still holds a reference
    S-->>V: notify again, into a dead view

The subject holds a strong reference to every listener, so a listener's lifetime becomes the subject's lifetime. A long-lived subject — a cache, an event bus, a static registry — plus short-lived subscribers is a monotonic leak, and the second symptom is worse than the memory: dead views still receive callbacks and act on them.

// Registration returns the means to undo itself.
public interface Subscription extends AutoCloseable { void close(); }

public Subscription subscribe(Listener l) {
    listeners.add(l);
    return () -> listeners.remove(l);
}

// Caller owns the lifetime and cannot forget symmetrically.
try (Subscription s = subject.subscribe(this::onChange)) {
    ...
}   // unsubscribed deterministically

Returning a subscription handle is the fix that survives contact with real code, because it removes the requirement that the caller remember an remove call and hold the identical reference. That second part is the detail people miss: a listener registered as a lambda or method reference cannot be removed later, since you no longer have the same object to pass to removeListener.

Two further consequences to name. Notification runs on whichever thread published the change, so a listener doing slow work blocks the publisher and one throwing an exception can prevent the remaining listeners from being called at all — the subject should isolate each callback. And the update order across listeners is unspecified, so any logic depending on it is a latent bug.

Weak references are the other common answer and they are a partial one: they stop the leak and introduce non-deterministic delivery, since a listener may be collected while still logically wanted.

Show me an enum with behaviour replacing conditionals.

The same conditional on a status appears in four files, each handling a different subset.

// Before: the knowledge is spread across every caller.
boolean canCancel(Order o) {
    return o.status() == PLACED || o.status() == PAID;
}
boolean canRefund(Order o) {
    if (o.status() == SHIPPED) return true;
    if (o.status() == DELIVERED) return true;
    return false;
}
// Adding PARTIALLY_SHIPPED requires finding all of these. Nothing fails if
// you miss one; the new status just silently behaves like the default.
enum OrderStatus {
    PLACED    (true,  false),
    PAID      (true,  false),
    SHIPPED   (false, true ),
    DELIVERED (false, true ),
    CANCELLED (false, false);

    private final boolean cancellable, refundable;
    OrderStatus(boolean c, boolean r) { cancellable = c; refundable = r; }
    boolean canCancel()  { return cancellable; }
    boolean canRefund()  { return refundable;  }
}

The win is that adding a constant forces a decision at the point of definition, because the constructor demands the values. The old version let a new status fall through to a default and behave plausibly wrong, which is the failure mode of distributed conditionals: nothing breaks loudly.

Where behaviour is more than a flag, give the enum an abstract method and let each constant implement it, which gets you polymorphic dispatch with a closed set of instances — a strategy whose implementations are enumerable and therefore exhaustively checkable by the compiler in a switch.

The limits are worth stating unprompted. An enum cannot be extended by another module, so this is only right when the set is genuinely closed and owned by you. And an enum should not accumulate persistence or presentation concerns; once constants need repository access or localised text, the behaviour belongs in a strategy resolved by the enum rather than inside it.

When is the state pattern better than a status enum with conditionals?

When the transitions are the complicated part, not the per-state attributes. An enum with behaviour handles "what may I do in this state" cleanly; the state pattern earns its place when each state must decide which state comes next for several different events, because then each state class owns its own transition table and an illegal transition is simply a method the class does not implement meaningfully. The cost is a class per state and a diffuse picture of the machine as a whole, which is why a small machine is better served by an explicit transition map you can print and read in one screen.

What does the command pattern give you beyond a method call?

A first-class object representing an intention, which is what makes queuing, logging, retrying, scheduling and undo possible. The undo case is the one to work: each command captures the information needed to reverse itself, so a stack of executed commands becomes an undo history and a second stack becomes redo. The requirement people forget is that a command must capture enough state to reverse its own effect — storing "the previous value" rather than "decrement by one" — because by the time undo runs, other commands may have changed the world. Its cost is a class per operation and a serialisation concern if commands outlive the process.

What is the visitor pattern's trade?

It lets you add a new operation over a fixed object structure without touching the classes in it, which is exactly the opposite trade from ordinary polymorphism. Adding an operation is cheap — one new visitor — and adding a node type is expensive, because every existing visitor must gain a method. So visitor is right when the type hierarchy is stable and the operations multiply, which is why compilers and AST tools use it and business domains rarely should. The secondary cost is the double dispatch: the accept method on every node is boilerplate, and the traversal logic ends up duplicated across visitors unless you factor it out.

What is chain of responsibility good for, and how does it fail?

A sequence of handlers each deciding whether to handle a request or pass it along, which suits pipelines where the set of steps varies — validation, authentication, filtering, middleware. The benefit is that each handler is independent and the order is data rather than code. The failure modes are specific: a request that no handler claims disappears silently unless there is a terminal handler, and a handler that both handles and forwards produces duplicated effects. Debugging is also harder than a straight call, because the path taken depends on runtime state, so logging which handler terminated the chain is not optional.

What problem is the mediator pattern solving?

Many-to-many coupling between peers. When ten components each know about the others to coordinate, you have up to ninety references and no place where the interaction rules live; a mediator makes each component know only the mediator, which then owns the coordination. The classic case is a dialog where enabling one control depends on three others. The obvious risk is that the mediator becomes the god class, holding all the logic that was previously spread out — which is an improvement only if it is genuinely one responsibility. When it is not, an event bus with independent subscribers is often the better decomposition.

Modelling state and invariants

Show me a state machine against the equivalent boolean tangle.

Four booleans is sixteen combinations, of which perhaps five are legal.

class Order {
    boolean paid, shipped, cancelled, refunded;
}
// paid && cancelled && !refunded  -> money taken, order dead, nobody notified
// shipped && !paid                -> goods gone, unpaid
// cancelled && shipped            -> which is it?
// Every method must defend against states the type permits and the business
// forbids, and each defence is a different developer's guess.
flowchart LR
    P[Placed] --> A[Paid]
    P --> C[Cancelled]
    A --> S[Shipped]
    A --> R[Refunded]
    S --> D[Delivered]
    D --> R

The diagram makes the point the booleans hide: there are six states and six legal transitions, not sixteen combinations. Replacing the flags with a single OrderStatus field makes the illegal eleven-sixteenths unrepresentable, and that is a much stronger guarantee than validating them.

The second gain is that the transitions become the only mutating API. Instead of setPaid(true), the order exposes pay(), which checks the current state and either advances it or refuses — so the rule "you cannot ship an unpaid order" lives in one method rather than in every caller that sets a flag.

Two details separate a good answer. Guard the transition, not the state: the question a transition method answers is "is this move legal from where I am", which is local and cheap, whereas validating a combination of flags requires knowing the whole matrix. And record transitions rather than only the current value if you will ever be asked when something shipped, because a status column destroys its own history on every update.

The trap in the diagram is the concurrent case. Two requests both reading Paid and both calling ship() need the transition to be atomic — a conditional update on the current status, or a lock — otherwise the state machine is correct in the class and violated in the database.

What is the difference between a value object and an entity?

An entity has identity that persists through change: a customer with a new name and address is the same customer, so equality is by identifier. A value object has no identity and is defined entirely by its attributes, so two Money instances of £5 are interchangeable and equality is by value. The practical consequences are three: value objects should be immutable and can be freely shared and cached, entities need identity assigned and a lifecycle, and mutating a value object is almost always a modelling error where you meant to replace it. Most codebases model too many things as entities.

Show me a value object enforcing its own invariant.

Construction is the only place an invariant can be enforced once and for all.

public final class Email {
    private final String value;

    public Email(String raw) {
        if (raw == null) throw new IllegalArgumentException("email required");
        String v = raw.trim().toLowerCase(Locale.ROOT);   // normalise, then check
        if (!v.matches("[^@\\s]+@[^@\\s]+\\.[^@\\s]+")) {
            throw new IllegalArgumentException("not an email: " + raw);
        }
        this.value = v;
    }

    public String value() { return value; }
    @Override public boolean equals(Object o) { /* by value */ }
    @Override public int hashCode()           { /* by value */ }
    @Override public String toString()        { return value; }
}

The type now carries a proof. Anywhere an Email appears, downstream code knows it is present, trimmed, lower-cased and structurally valid, so the validation does not need repeating in the controller, the service and the mailer — and cannot be forgotten in the fourth place that constructs one.

Immutability plus the constructor check is what makes that guarantee permanent. If the class had a setter, the proof would expire immediately after construction, which is why a "validated" mutable object is not the same thing at all.

Normalising before validating is the detail that makes it useful in practice. Doing it in this order means " Bob@Example.COM " and "bob@example.com" become equal, so the uniqueness check in your database and the equality check in your code agree — and if they do not, you get duplicate accounts that look identical in the admin UI.

Two follow-ups to have ready. For input validation you often want to collect all the errors rather than throw on the first, so a static Email.parse returning a result type sits alongside the throwing constructor. And the constructor should not do anything else — no MX lookup, no database check — because a constructor that performs I/O makes the type impossible to use in a test or a loop.

What does "make illegal states unrepresentable" mean?

Choosing types so that a wrong value cannot be constructed, rather than writing checks that catch it. A nullable deliveredAt on an order that may not be delivered permits "cancelled with a delivery date"; a sealed hierarchy where only Delivered carries a timestamp does not. The gain is that the number of states your code must handle collapses, so the defensive checks scattered through the codebase become unnecessary rather than merely passing. The limit is the type system you have and the boundary you are at: data arriving over the wire is untrusted, so you validate once at the edge and construct the constrained type from it.

What is primitive obsession, and what does it cost?

Representing domain concepts as strings, integers and maps rather than types. transfer(String from, String to, long amount) compiles when the arguments are swapped, and long amount says nothing about currency or minor units, so a currency mismatch is a runtime discovery in an accounting report. The cost is that validation and behaviour have nowhere to live, so they end up duplicated at every call site and drift. Replacing them with AccountId and Money moves whole classes of bug to compile time and gives the arithmetic — rounding, currency checks, allocation of a remainder — one home. The cost is more small types, which is usually a good trade.

What does immutability actually buy, and what is defensive copying?

Immutability buys you that a reference's meaning cannot change under you, which removes aliasing bugs, makes objects safe to share across threads without synchronisation, and makes them safe to use as map keys and in sets. Defensive copying is what you must do when a nominally immutable object holds a mutable one: taking a List in the constructor and storing the reference means the caller can still mutate your contents, so you copy on the way in — and on the way out of a getter, or return an unmodifiable view. The subtlety is depth: copying a list of mutable objects protects the list, not the objects.

Why can an object's constructor not be trusted to leave it valid?

Because in most languages construction can leak a partly-built object. A constructor that registers this with a listener registry, starts a thread, or calls an overridable method publishes the instance before subclass fields are assigned and before final validation runs, so another thread or a subclass can observe an invalid state. That is why the rules are to do no work in a constructor beyond validating and assigning, to avoid calling overridable methods from it, and to use a static factory when construction needs several steps — the object then becomes visible only when it is finished.

API and class design

Show me equals and hashCode broken by a mutable key.

The entry is in the map. Looking it up returns nothing.

class Point {
    final int x, y;
    Point(int x, int y) { this.x = x; this.y = y; }
    @Override public boolean equals(Object o) { /* compares x and y */ }
    @Override public int hashCode() { return Objects.hash(x, y); }
}

Point p = new Point(1, 2);
Map<Point, String> map = new HashMap<>();
map.put(p, "origin-ish");

p.y = 99;                       // the key mutated while inside the map

map.get(p);                     // null   - hashed to a different bucket
map.get(new Point(1, 99));      // null   - that bucket holds nothing
map.containsKey(p);             // false  - yet the entry is still in the map
map.size();                     // 1      - and it can never be retrieved

The map computed the bucket from the hash at insertion time and does not know the key changed. The entry is now unreachable by any key, and it also cannot be removed, so this is a memory leak as well as a correctness bug.

The contract being broken is that hashCode must be consistent while the object is in a hash-based collection. The fix is not a cleverer hashCode — it is that anything used as a key must be immutable, which is one of the strongest practical arguments for value objects being final with final fields.

The rest of the contract is worth reciting alongside it, because interviewers ask for it. Equality must be reflexive, symmetric and transitive; equal objects must have equal hash codes, while unequal objects may collide; and both must depend on the same fields. Symmetry is the one broken most often, by an equals that accepts a subclass — a.equals(b) true and b.equals(a) false, which makes collection behaviour depend on argument order.

The related trap is entity equality. Defining equals on an entity by its business fields means two unsaved objects are equal, and defining it by a database-generated identifier means a newly created object equals nothing until it is persisted — including itself before and after saving, if the collection cached its hash.

Show me a method signature people cannot misuse.

Every parameter that can be passed wrongly eventually is.

// Misusable in five distinct ways.
void transfer(long from, long to, double amount, boolean notify, boolean async);

// transfer(a, b, ...) and transfer(b, a, ...) both compile: direction is
//   positional and both ids are long
// double amount: 0.1 + 0.2 for money, and no currency at all
// two adjacent booleans: swapping them compiles and reverses both behaviours
// nothing prevents amount being negative, which reverses the transfer
void transfer(AccountId from, AccountId to, Money amount, TransferOptions opts);

// AccountId is a distinct type, so a swap still compiles - so make it harder:
void transfer(Transfer request);      // request is built by a named builder
//   Transfer.from(a).to(b).of(Money.gbp(2500)).notifying(RECIPIENT).build()

Three techniques do the work here. Distinct types for distinct concepts kill whole classes of transposition and make the currency explicit. Replacing boolean parameters with an enum or an options object makes the call site readable, since notifying(RECIPIENT) says what true did not. And validating in the value type — Money refusing a negative amount — means the method needs no guard clause.

The principle underneath is that the signature is where you choose between a compile error and a production incident. A parameter that can only be checked at runtime has to be checked by every caller, and one of them will not.

Two more habits belong in the same answer. Do not return null from a method callers will chain on, and do not accept null as a meaningful argument value — both push a branch onto every caller. And make the dangerous operation the longer one to write: if delete(id) and deleteAll() sit next to each other, the second should require something explicit, because the cost of the two mistakes is not symmetrical.

Show me a class diagram for one small design.

A cinema seat booking, reduced to the classes that carry a rule.

flowchart TD
    B[Booking] --> S[Seat]
    B --> M[Money total]
    B --> ST[BookingStatus]
    B --> PP[PricingPolicy interface]
    PP --> A[AdultPricing]
    PP --> C[ConcessionPricing]
    S --> SC[Screening]

The diagram is doing one job: showing where the behaviour lives. Booking owns the invariant that its seats are all in one screening and that its total matches its lines, which is why Money and BookingStatus hang off it rather than sitting in a service.

PricingPolicy as an interface with two implementations is the only abstraction in the picture, and it is there because pricing is the axis that actually varies — concessions, promotions, dynamic pricing. Everything else is a concrete class, which is the right default: an interface per class would make this diagram twice the size and no clearer.

What the diagram deliberately omits is worth saying aloud in an interview. There is no BookingManager, no SeatUtils and no DTO layer, because none of them carries a rule. In a live round, drawing seven classes where each one owns something reads far better than drawing twenty where most are data.

The seat-to-screening arrow is where the hard question lives: two users choosing the same seat. That is a uniqueness constraint on (screening, seat) at the point of persistence, plus a short-lived hold, and saying so unprompted is what separates a class diagram from a design.

How should null be handled, and where does Optional fit?

By eliminating it where you can and being explicit where you cannot. A field that is genuinely optional is better modelled as an empty collection, a null-object implementation, or a distinct subtype than as a nullable reference every caller must remember to check. Optional is a good return type for a lookup that may legitimately find nothing, because it makes absence part of the signature. It is a poor field type and a poor parameter type — it adds an allocation and a second kind of emptiness, since an Optional parameter can itself be null. The non-negotiable rule is never to return null from a method that returns a collection.

When should an exception be checked rather than unchecked?

Checked when the caller can plausibly do something about it and the situation is part of the normal range of outcomes — a payment declined, a file absent, a concurrent edit. Unchecked when it indicates a programming error or a condition no caller can recover from, which is most of them. The practical failure of checked exceptions is that they propagate up through every intermediate signature and get swallowed or wrapped by developers who only wanted to compile, so the information is lost anyway. Many modern codebases use unchecked throughout and handle recovery at a boundary, which is defensible if the boundary is deliberate.

What belongs in a domain exception?

Enough for the caller to decide what to do, and nothing about the mechanism. InsufficientFunds carrying the account, the requested amount and the available balance lets the caller render a useful message and lets a test assert on it; a RuntimeException("error") forces string matching. It should not carry the vendor's error code or a SQL state, because that couples your domain to the infrastructure — translate at the adapter. Two habits go with it: always preserve the original exception as the cause when wrapping, and do not use exceptions for expected control flow on a hot path, since constructing a stack trace is expensive.

What makes an interface worth extracting?

More than one real implementation, or a boundary you genuinely need to stand at — an external system, a layer edge, a plugin point. Extracting one because a class might be replaced someday costs a file, a level of indirection and a name that must be kept meaningful, and buys an option nobody exercises. The honest signal is that you have a second implementation now, including a test double that is doing real work rather than mocking every method. The related smell is an interface whose only implementation is named after it with an Impl suffix, which is a strong hint the abstraction was speculative.

Why should a class be immutable by default?

Because most defects in object-oriented code come from state changing when someone did not expect it, and immutability removes the category. An immutable object is thread-safe without synchronisation, safe to cache and share, usable as a map key, and impossible to observe half-updated. The cost is allocation on every change, which is almost always irrelevant and occasionally is not — a tight loop mutating a large structure is a real exception. The practical rule is that value objects and DTOs should be immutable without argument, while entities whose whole purpose is a lifecycle need controlled mutation through named transitions.

Interview traps

Show me the parking lot broken into classes.

The classic prompt, answered as a structure rather than a list of nouns.

Entities and values
  ParkingLot          the aggregate: floors, admits a vehicle, releases it
  Floor               holds spots, knows its own free count per spot type
  Spot                id, SpotType, occupied-by; enforces fits(vehicle)
  SpotType            enum MOTORCYCLE, COMPACT, LARGE, ACCESSIBLE
  Vehicle             abstract; Motorcycle, Car, Van - each knows its size
  Ticket              id, spot, vehicle, issuedAt   (a value, immutable)

Policies behind interfaces
  SpotAssignment      strategy: nearest-to-entrance, or by floor balance
  PricingPolicy       strategy: hourly, day-rate, free-first-15-minutes
  Payment             interface: cash, card - one adapter each

Services
  EntryGate           issue ticket = assign spot + mark occupied, atomically
  ExitGate            price ticket + take payment + free spot

The scoring is mostly in three decisions rather than the class list. The first is where fits lives: putting it on Spot as a function of SpotType and vehicle size means adding an electric-vehicle spot is one enum constant and one rule, whereas a switch in the gate service means finding every switch.

The second is making assignment and pricing interfaces. The interviewer will ask "what if we want to charge differently at weekends" or "assign the nearest spot", and having named those as the two axes of variation up front means the answer is "a second PricingPolicy" rather than a redesign.

The third is concurrency, which is where most candidates stop short. Two cars at two gates can be assigned the same spot unless the claim is atomic — a compare- and-set on the spot's state, or a lock per floor, and saying which you would use and why is the strongest thing you can add.

What to leave out deliberately: no ParkingLotManager, no getters and setters enumerated, no database schema unless asked. Say aloud that you are treating persistence as out of scope so the time goes on the domain, because otherwise the interviewer cannot tell the difference between a choice and an omission.

How do you approach a "design a vending machine" question?

Start from the state machine, because that is what the question is really about: idle, collecting money, dispensing, refunding, out of service. Enumerate the events — coin inserted, selection made, dispense complete, cancel — and say what each does in each state, which immediately surfaces the interesting cases like cancelling mid-payment and a selection that is out of stock after money is taken. Then name the value objects, Money and Coin, and the strategies, change-making and pricing. Candidates who begin with classes rather than states end up with a VendingMachineService full of flags.

What does an interviewer want from "design an elevator"?

The scheduling policy as a replaceable thing, and honesty about which one you chose. The classes are quickly agreed — Elevator, Request, Floor, Direction, a Controller — so the discussion is in how the controller picks the next stop, and the expected answer is that the naive nearest-request policy starves people and the standard fix is the elevator algorithm, sweeping in one direction while serving requests along the way. Model the policy behind an interface so you can say the two implementations out loud. The other rewarded detail is separating a hall call from a car call, since they have different semantics.

What is the most common way candidates fail an LLD round?

Producing a data model and calling it a design. Six classes of fields with getters and setters, plus a service holding all the behaviour, is a procedural program in object syntax, and it fails because none of the classes protects anything. The second most common failure is the reverse: an interface, a factory and an abstract base for every concept before any requirement has forced it, so the design cannot be read and every question is answered with more indirection. The middle path is concrete classes owning their invariants, with abstraction introduced only at the axes the interviewer has actually named.

Should you write real code or pseudocode in an LLD round?

Write real signatures and the bodies that carry a rule; skip the bodies that do not. The interviewer is reading whether your types make sense, so a full method signature with parameter types is high value and a getter is not. Say which parts you are eliding and why — "I will leave the persistence bodies out and write the transition methods properly" — because unstated shortcuts read as gaps. Keep the naming disciplined even in a hurry: a class called Data or a method called process costs you more than the two seconds it saved, since the interviewer's whole signal is whether you can name things.

What single question separates candidates in an LLD round?

"What happens when two of these run at the same time?" It cannot be prepared generically, because the answer depends on the design just drawn, and it is where a clean class diagram meets reality. A strong answer names the exact shared resource, the interleaving that breaks it, and the mechanism chosen — a uniqueness constraint at the point of persistence, a conditional update on the current state, or a lock with a stated scope — and then says what the caller sees when it loses the race. A weak answer says the method is synchronised, which protects one process and one instance and nothing else, or that the database handles it, which is a hope rather than a design.