Low-Level Design — Cheat Sheet
Interview Q&A · 100 topics. Download the PDF or the Instagram carousel and share it.
What is encapsulation and what does it actually buy you?
Encapsulation is bundling data with the behaviour that operates on it, and controlling access so the internal representation can change without breaking callers. The part people miss is that private fields plus getters and setters for every one of them is not encapsulation. If every field is exposed through an accessor pair, the internal representation is effectively public and you have gained nothing except ceremony. Real encapsulation means the class exposes operations, not state. A BankAccount should have deposit and withdraw, not setBalance. The moment you expose setBalance you have lost the ability to enforce that the balance never goes negative, because any caller can set anything. The payoff is that invariants hold. If all mutation goes through methods, those methods can validate, and you can reason about the object being in a valid state at all times. The secondary payoff is freedom to refactor. If callers only depend on behaviour, you can change how the data is stored — split a field, compute it, move it to another object — without touching them. The test to apply: can an outside caller put this object into an invalid state?
When should you use composition instead of inheritance?
Almost always, and inheritance only when there is a genuine is-a relationship with substitutability. Inheritance couples you to the parent's implementation, not just its interface. A change in the base class can break subclasses in ways the compiler will not catch — the fragile base class problem. It is also fixed at compile time, single in most languages, and forces you to accept the entire parent interface whether it makes sense or not. Composition means holding a reference to a collaborator and delegating. It is flexible, can change at runtime, and lets you take exactly the behaviour you want. The canonical illustration is a Stack extending ArrayList. Because it inherits the whole list interface, callers can insert at arbitrary positions and violate the stack invariant. Composition — a Stack holding a list privately — exposes only push and pop and the invariant holds. Use inheritance when subtypes are genuinely substitutable for the parent everywhere, when you control both sides, and when the hierarchy is stable. Even then, prefer inheriting from interfaces or abstract classes designed for extension rather than from concrete classes.
What is the Liskov Substitution Principle and what does violating it look like?
A subtype must be usable anywhere its supertype is expected, without the caller needing to know the difference or the program becoming incorrect. The classic violation is Square extending Rectangle. Rectangle has independent width and height; Square must keep them equal. So setWidth on a Square must also change the height, which breaks any code that sets width and height independently and expects both to hold. The code was correct for Rectangle and is wrong for Square — the substitution failed. Other violations: a subclass that throws an exception the parent does not declare, one that strengthens preconditions by rejecting inputs the parent accepts, or one that weakens postconditions by returning less than the parent promised. The subtle one is a subclass that overrides a method to do nothing, or to throw UnsupportedOperationException. That is Java's immutable collections in a nutshell, and it means callers cannot rely on the List contract. The practical test: can you replace the parent with the child in existing code and have every test still pass, without reading the child's source? If not, the hierarchy is wrong and composition is likely the answer.
What is the difference between an abstract class and an interface, and when do you use each?
An interface declares a contract with no state. An abstract class can hold fields, constructors and implemented methods alongside abstract ones. Since Java 8, interfaces can have default methods, which narrows the gap — but they still cannot hold instance state, and that is the essential difference. Use an interface to define a capability that unrelated types may implement. Comparable, Runnable and Serializable are capabilities, not family membership. A class can implement many interfaces, so this composes. Use an abstract class when subclasses genuinely share implementation and state, and when you want to control the extension points — a template method that calls abstract hooks is the classic case. The practical guidance is to default to interfaces, because they impose less and a class can only extend one thing. Reach for an abstract class when you find yourself duplicating the same implementation across several implementers and that duplication is genuinely shared behaviour rather than coincidence. Default methods are best used for backwards compatibility — adding a method to a published interface without breaking implementers — rather than as a way to smuggle in an abstract class.
What is polymorphism and how does it change how you write code?
Polymorphism lets one interface serve many concrete types, with the actual behaviour chosen at runtime by the object rather than by the caller. The practical effect is that it replaces conditionals with dispatch. Code that switches on a type field — if it is a circle do this, if a square do that — becomes a call to a method each type implements. Adding a new shape then means adding a class, not editing every switch statement scattered through the codebase. That is the connection to the Open/Closed Principle: polymorphism is the mechanism that makes extension without modification possible. The signal to look for in a code review is a switch or if-else chain on a type, especially one that appears more than once. Repeated switching on the same discriminator is the strongest indicator that you have a missing polymorphic type. The caution is that polymorphism is not free. It spreads behaviour across classes, so understanding what happens for a given input means finding the right implementation. When there are only two cases and they will never grow, a conditional is clearer than a hierarchy — a genuine judgement call rather than a rule.
What is the difference between an anemic domain model and a rich one?
An anemic model has classes that are little more than data holders — fields with getters and setters — while all the behaviour sits in service classes that operate on them. A rich model puts behaviour on the objects that own the data. An Order knows how to add a line, calculate its total, and refuse to be cancelled once shipped. Anemic models are extremely common, partly because ORMs and serialisation frameworks encourage them. Martin Fowler calls it an anti-pattern because it is procedural code wearing object-oriented clothing: you have the ceremony of classes without encapsulation. The cost is that invariants cannot be enforced. If Order exposes setStatus, nothing prevents a caller setting a shipped order back to draft, and the rule about when that is allowed ends up duplicated in every service that touches orders — or missing from one of them. The defence of anemic models is that they suit simple CRUD, where there is genuinely no behaviour, and that they keep persistence concerns separate. The honest position: for a domain with real rules, put the rules on the objects. For a thin data-shuffling layer, the ceremony of a rich model buys nothing.
What is the difference between coupling and cohesion?
Coupling measures how much one module depends on another. Cohesion measures how strongly the elements within a module belong together. You want low coupling and high cohesion, and the two are related: splitting things that do not belong together reduces the reasons for other modules to depend on you. High cohesion means a class has one clear purpose and everything in it serves that purpose. A class with a method for parsing dates, one for sending email and one for calculating tax has low cohesion — it will change for three unrelated reasons and everyone will depend on it. Low coupling means depending on abstractions rather than concrete types, depending on few things rather than many, and not reaching through one object to get at another. Depending on an interface you defined is looser than depending on a third-party class. The practical diagnostic is change impact. If a small requirement change touches many files, coupling is too high. If one file changes for many unrelated reasons, cohesion is too low. Most design principles — single responsibility, dependency inversion, interface segregation — are ways of describing these two properties.
Why should you favour immutability, and what does it cost?
An immutable object cannot change after construction, so it is thread-safe with no synchronisation, cannot be corrupted by a caller, is safe to share and cache, and can be used as a map key without the risk of its hash changing. It also removes an entire category of bug: an object handed to a method and quietly modified. Defensive copying exists to work around mutability, and immutability makes it unnecessary. The reasoning benefit is the biggest one. If a value cannot change, you do not have to trace where it might have been modified — the constructor is the only place it is set. The costs are allocation and awkwardness. Every modification creates a new object, which pressures the garbage collector on hot paths. Building an object with many fields requires either a long constructor or a builder. And deeply nested immutable structures are tedious to update, which is what persistent data structures and lenses address in functional languages. The pragmatic position: make value objects immutable by default — Java records make this cheap — and allow mutability for entities with a genuine lifecycle and for performance-critical internals.
What is the difference between a value object and an entity?
An entity has identity that persists through change. A User with ID 42 is the same user whether their name or email changes. Equality is by identity, not by attributes. A value object has no identity — it is defined entirely by its attributes. Two Money objects of 100 rupees are interchangeable; there is no meaningful sense in which one is a different hundred rupees. Equality is by value. The practical consequences are significant. Value objects should be immutable, since changing an attribute makes it a different value. They should implement equals and hashCode by attributes. And they can be freely shared and cached. Entities need an identity field, equality based on it, and a lifecycle — created, modified, deleted. The design mistake worth naming is treating everything as an entity because that is what the database table suggests. An address stored in a table with an ID is usually still a value object conceptually, and modelling it as one — immutable, compared by value — makes the code simpler. Java records are an excellent fit for value objects, generating equality and immutability correctly.
What does "program to an interface, not an implementation" actually mean in practice?
Declare variables, parameters and return types using the most general type that satisfies your needs, so callers and collaborators are not coupled to a concrete class. Concretely: declare List rather than ArrayList, accept Collection rather than List when you only iterate, and return an interface from a factory rather than the concrete type. The benefit is substitutability. If a method accepts a List, you can pass an ArrayList, a LinkedList, or an immutable list, and the method continues to work. If it accepts ArrayList, you have needlessly excluded every alternative. The deeper application is your own abstractions: depend on a PaymentGateway interface rather than on StripeGateway, so the implementation can be swapped and tested with a fake. The caution is not to over-apply it. Creating an interface with exactly one implementation, purely because a rule says to, adds indirection without benefit — the interface exists only to be mocked, which is a weak justification. The useful version of the rule is: abstract where variation is plausible or where you need a seam for testing, and use concrete types where the type genuinely is fixed.
What is the Law of Demeter and is it worth following?
The Law of Demeter says a method should only call methods on itself, its parameters, objects it creates, and its own fields — not on objects returned by those calls. The symptom it targets is the train wreck: order.getCustomer().getAddress().getCity().getName(). That chain couples the caller to four classes and the structure connecting them. Change any link and this code breaks, even though it only wanted a city name. The fix is to add a method at the appropriate level — order.getCustomerCity() — so the traversal happens where the knowledge belongs. It is worth following as a heuristic rather than a law. Long chains through domain objects genuinely signal misplaced responsibility, and the code reads better after the fix. But it is routinely violated harmlessly, especially with fluent builders and streams, where chaining is the point and the objects are not domain structure. Applying it dogmatically produces classes full of pass-through delegating methods, which is its own kind of noise. The useful test is whether the chain reveals structure the caller should not know about. Chaining on a builder does not; chaining through three domain aggregates does.
How do you decide what should be a class?
The useful heuristic is that a class should represent one concept with one reason to change, and should own the data its behaviour needs. Start from the domain language. If people working in the domain talk about an Order, a Reservation, a Shipment, those are candidate classes — and using their vocabulary means the code reads like the problem. Then apply the responsibility test. Describe what the class does in one sentence without using "and" or "or". If you cannot, it is doing too much. The other direction matters too: not everything needs a class. A class with one method and no state is usually just a function, and wrapping it adds ceremony. Java requires a class as a container, but that does not make it a meaningful abstraction. Watch for classes that are named after patterns or layers rather than concepts — OrderManager, DataHelper, ProcessingUtil. Those names are a signal that the responsibility was never identified, and such classes accumulate everything nobody knew where to put. If you cannot name it precisely, you probably have not found the concept yet.
Explain the Single Responsibility Principle with a concrete example.
A class should have one reason to change. The reason-to-change framing matters more than "does one thing", which is too vague to apply. The example: a Report class that gathers data, formats it as HTML, and emails it. That has three reasons to change — a query change, a presentation change, and a delivery change — and three different people or teams might request them. Split into a ReportDataSource, a ReportFormatter and a ReportSender. Now a change to the email provider touches one class, and the data logic is untouched and untested-against unnecessarily. The practical benefits are that changes are localised, tests are focused, and merge conflicts drop because different concerns live in different files. The over-application to watch for is splitting until every class has one method. That produces a codebase where following a single operation means opening fifteen files, which is worse than the problem. Responsibility is at the level of a reason to change, not a line of code. The practical test: when a requirement changes, how many classes do you touch? Consistently touching several for one conceptual change suggests responsibilities are misaligned.
What is the Open/Closed Principle and how do you achieve it?
Software should be open for extension but closed for modification — you should be able to add behaviour without editing existing, tested code. The mechanism is polymorphism. If a discount calculator switches on a customer type, adding a type means editing that switch, and every other switch on customer type scattered through the codebase. If instead each type implements a DiscountPolicy interface, adding a type means adding a class and nothing existing is touched. Strategy, template method and the plugin pattern are all applications of this. The honest caveat is that you cannot be open to every kind of change. Making a design extensible along one axis usually makes it rigid along another — a hierarchy open to new types is closed to new operations, which is the expression problem. So the practical guidance is not to speculatively abstract everything. Wait until you see the axis of variation — the second or third instance of a change — and then refactor to make that axis extensible. Guessing wrong produces abstraction that fits nothing. The payoff, when applied to the right axis, is that adding features stops carrying regression risk.
What is the Interface Segregation Principle?
Clients should not be forced to depend on methods they do not use. Prefer several small focused interfaces over one large one. The symptom is an implementation full of methods that throw UnsupportedOperationException or do nothing, because the interface demanded them and this implementer has no meaningful behaviour for them. The classic example is a Worker interface with work and eat. A RobotWorker must implement eat, which is meaningless. Splitting into Workable and Feedable lets each implementer take only what applies. Java's own libraries show the cost of getting it wrong: the immutable collections implement List and then throw on add and remove, which is both an ISP and a Liskov violation, and it means you cannot trust a List reference. The practical benefit is looser coupling. A client depending on a two-method interface is unaffected by changes to methods it never calls, and it is far easier to write a test double for. The balance to strike is against fragmentation: an interface per method produces a lot of types. Segregate along the lines of how clients actually use the abstraction, which usually gives a small number of coherent roles.
What is Dependency Inversion, and how does it differ from dependency injection?
Dependency Inversion is a design principle: high-level policy should not depend on low-level detail; both should depend on abstractions. And the abstraction should be owned by the high-level module, not the low-level one. That ownership detail is the part usually missed. If your OrderService defines a PaymentGateway interface and StripeGateway implements it, the dependency arrow now points from the detail toward the policy — inverted from the natural direction. If instead the interface ships with the Stripe library, you have not inverted anything, you have just added indirection. Dependency Injection is a technique for supplying dependencies from outside rather than constructing them internally — usually via the constructor. It is one way to satisfy the principle, but it is mechanism, not principle. You can use dependency injection and still violate dependency inversion, by injecting a concrete class. The practical payoff of doing it properly is that the domain has no compile-time dependency on infrastructure, so it can be tested without a database and the infrastructure can be replaced. That is the core idea behind hexagonal architecture.
Are the SOLID principles always worth following?
No, and being able to say so thoughtfully is usually what an interviewer is listening for. They are heuristics developed for large, long-lived, changing codebases, and their benefit scales with size and lifespan. Applied to a small script or a genuinely simple CRUD layer, they add indirection that costs more than it saves. The common failure is premature abstraction: an interface for every class, a factory for every construction, and a strategy for behaviour that has exactly one variant and always will. That codebase is technically SOLID and considerably harder to read than the direct version. The cost is real. Every abstraction is a level of indirection a reader must traverse, and speculative abstractions are usually wrong because you guessed the axis of variation before seeing it. The defensible position is to write the simple thing first, and refactor toward these principles when you observe the pressure — the second implementation, the third reason a class changes, the test you cannot write. That is also honest about where they came from: they describe what well-factored code tends to look like, not a procedure for producing it.
How do SOLID principles relate to testability?
Closely enough that difficulty writing a test is usually a design signal rather than a testing problem. Dependency inversion is the direct link. A class that constructs its own database connection cannot be tested without a database. The same class receiving a repository interface can be tested with a fake in microseconds. The seam that makes it testable is the same seam that makes it flexible. Single responsibility makes tests focused: a class with one reason to change has a small, comprehensible test suite, while a class doing three things needs tests covering their combinations. Interface segregation makes test doubles cheap. Stubbing a two-method interface is trivial; stubbing a twenty-method one is why people reach for mocking frameworks and end up with brittle tests. The useful inversion of this: when a test is hard to write — needing extensive mocking, elaborate setup, or reaching into statics — treat it as feedback about the design rather than reaching for a more powerful testing tool. PowerMock existing to mock static methods is a good example of a tool that lets you avoid fixing the design. Hard-to-test usually means tightly coupled.
What is the difference between DRY and premature abstraction?
DRY says every piece of knowledge should have one authoritative representation. The failure mode is deduplicating code that looks similar but represents different knowledge. Two methods with identical bodies today may exist for entirely different reasons. Merging them couples two concepts that will diverge, and when they do you add a boolean parameter, then another, and the shared function becomes a maze of conditionals serving two callers badly. Sandi Metz's formulation is the one worth quoting: duplication is far cheaper than the wrong abstraction. Duplicated code is easy to change; a wrong abstraction with several callers is not. The practical rule is the rule of three — wait until the third occurrence before extracting, because two points do not establish a pattern and you cannot see the axis of variation yet. The distinguishing question is whether the two pieces would change for the same reason. If a requirement change would necessarily alter both identically, that is genuine duplication of knowledge and should be unified. If one could change without the other, the similarity is coincidental and should be left alone. DRY is about knowledge, not about characters.
What is the Dependency Injection container doing, and do you need one?
A DI container constructs your object graph — reading configuration or annotations to work out what each class needs, instantiating dependencies in the right order, and managing their lifecycles. What it buys you at scale is not having to hand-wire hundreds of objects, plus lifecycle management — singletons, per-request scopes — and cross-cutting concerns applied by proxying, such as transactions and caching. What it costs is indirection and magic. Construction happens at runtime through reflection, so errors surface at startup rather than compile time, and following what is injected where requires understanding the container. Spring's proxying in particular produces behaviour that surprises people — self-invocation bypassing @Transactional being the classic case. You do not need a container to do dependency injection. Constructing objects explicitly in a composition root is dependency injection, it is compile-time checked, and for a small application it is clearer. The honest position: containers earn their place in large applications with deep graphs and cross-cutting concerns. For a small service, manual wiring in main is simpler and has no magic — and the principle is satisfied either way.
What is the composition root and why does it matter?
The composition root is the single place in an application where the object graph is constructed and dependencies are wired together — typically in main, or in the container configuration. It matters because it is what keeps dependency injection honest. If objects are constructed throughout the codebase, classes are coupled to concrete implementations wherever construction happens, and the benefit of injection is lost. Concentrating construction in one place means every other class receives its collaborators and never news up a dependency. That is what makes them substitutable and testable. It also gives you one place to understand the application's structure. Reading the composition root tells you what the system is made of and how it fits together, which is genuinely useful when joining a codebase. The practical rule that follows: the new keyword for a dependency should appear only in the composition root or in factories. Seeing new SomeService() inside business logic is the signal that a dependency has been hard-wired. Value objects and data structures are the exception — creating a new Money or a new ArrayList inside a method is fine, because those are not dependencies you would ever substitute.
How do you avoid a god class?
A god class knows and does too much — hundreds of methods, dozens of fields, and every other class depends on it. They form gradually. Nobody creates one deliberately; each addition seems reasonable because the class already has the data needed. The name is usually the first symptom: Manager, Processor, Handler, Util — names that describe no concept and therefore accept anything. Prevention starts with naming. A class named for a domain concept resists unrelated additions, because adding email sending to Order is obviously wrong in a way that adding it to OrderManager is not. Then watch the metrics that indicate drift: growing line count, growing field count, and — most tellingly — a change to the class for a reason unrelated to its last change. To break one up, look for clusters. Fields used by one subset of methods and not others usually indicate a class trying to emerge. Extracting that cluster into its own type with its own name is the standard refactoring. Do it incrementally behind tests. A god class is depended on by everything, so a big-bang split is high risk; extracting one responsibility at a time is safer and gives value immediately.
What is YAGNI and how does it conflict with good design?
You Aren't Gonna Need It: do not build functionality on speculation about future requirements. The justification is that speculative features are usually wrong, cost effort now, and add complexity that every subsequent change must work around. A configuration option added because someone might want it becomes a permanent branch in the code and a permanent test matrix entry. The apparent conflict with good design is that principles like Open/Closed seem to ask for extensibility, which is anticipation. The resolution is that extensibility should be discovered, not predicted. You do not build a strategy interface because you might want another algorithm; you build it when the second algorithm arrives, and the refactoring is cheap because tests cover the first. What YAGNI does not license is skipping design entirely. Clean boundaries, good names and separated concerns are not speculative features — they are what makes the future refactoring cheap. YAGNI applies to features and configurability, not to keeping code comprehensible. The useful test: am I adding this because a requirement exists, or because I imagine one might? The second is the one to resist.
How would you review a class design for quality?
Start with names. Does the class name denote a concept from the domain, and can you describe its responsibility in one sentence without "and"? Vague names indicate an unformed concept. Then look at the public surface. Does it expose behaviour or state? A class of getters and setters has no encapsulation, and its invariants are enforced nowhere or everywhere. Check the dependencies. How many collaborators does it need, and are they abstractions or concrete infrastructure? A constructor with eight parameters is telling you the class does too much. Look for conditionals on type, which indicate missing polymorphism, and for feature envy — methods that use another object's data more than their own, which suggests the behaviour is on the wrong class. Ask what happens when the obvious next requirement arrives. If the answer is "edit this switch in four places", the design is not open to that axis. And try to write a test in your head. If it needs a database, a clock, or six mocks, the coupling is too high. Most of these reduce to two questions: does it have one reason to change, and can I replace its collaborators?
How do you implement a thread-safe singleton, and should you?
The best implementation in Java is an enum with a single constant. The JVM guarantees one instance, handles serialisation correctly, and is immune to reflection attacks — none of which the other approaches get for free. The holder idiom is the alternative when you need lazy initialisation with a class: a private static nested class holding the instance, initialised on first access. Class loading guarantees thread safety with no synchronisation cost. Double-checked locking works but requires the field to be volatile, and it was genuinely broken before Java 5 because the memory model permitted a reference to be published before the constructor finished. It is more code and more risk than the holder idiom for no benefit. Synchronising the whole accessor is correct and simple but locks on every call. The more useful answer is whether you should. A singleton is global mutable state with a hidden dependency — callers reach for it directly, so it cannot be substituted in tests and the coupling is invisible in the signature. Most singletons are better expressed as a single instance managed by your DI container and injected, which gives one instance without the global access.
What is the Factory Method pattern and when do you use it?
Factory Method defines an interface for creating an object but lets subclasses decide which class to instantiate. The creation is deferred to a method that subclasses override. The motivation is that a class needs to create collaborators but should not be coupled to their concrete types. A DocumentApplication knows it needs a Document but not whether it is a SpreadsheetDocument or a TextDocument; each subclass supplies its own. It is distinct from a simple static factory method, which is just a named constructor — Integer.valueOf, List.of. That is useful for readability, caching and returning subtypes, but it is not the Gang of Four pattern. It is also distinct from Abstract Factory, which creates families of related objects through an object you pass around rather than through inheritance. In practice, factory methods are often replaced by dependency injection: rather than a subclass deciding what to create, the collaborator is injected. That is usually simpler, because it avoids an inheritance hierarchy created solely to vary one construction decision. The case where it still earns its place is a framework with a template method that needs a hook for object creation.
When is the Builder pattern the right choice?
When an object has many optional parameters, when construction is complex enough to need validation, or when you want an immutable object without a constructor of a dozen arguments. The problem it solves is the telescoping constructor — a chain of overloads with increasing parameter counts — which is unreadable at the call site because new Pizza(12, true, false, true, false) tells you nothing. A builder makes each value named at the call site, allows any subset of optional values without an overload per combination, and gives one place to validate before constructing — so the object can enforce its invariants in a private constructor and never exist in an invalid state. The cost is boilerplate: a builder class mirroring the fields. Records and named parameters reduce the need in modern code, and Lombok's @Builder removes the typing at the cost of generated code. Do not use it for objects with two or three required fields — a constructor is clearer. The detail worth mentioning is validating in build() rather than in each setter, so cross-field rules can be checked, and returning a genuinely immutable object rather than one the builder can still mutate.
What is the Abstract Factory pattern?
Abstract Factory provides an interface for creating families of related objects without specifying their concrete classes. The distinguishing feature is the family. A UIFactory might create a Button, a Checkbox and a Scrollbar; a WindowsFactory produces the Windows variants and a MacFactory the Mac ones. The guarantee is consistency — you cannot accidentally combine a Windows button with a Mac scrollbar, because they come from the same factory. That is the real value: enforcing that a set of objects used together belong to the same variant. The usual example beyond UI toolkits is database access, where a factory produces the Connection, Statement and ResultSet implementations for a particular vendor. The cost is rigidity in one specific direction. Adding a new product to the family — a Slider — means changing the factory interface and every implementation. Adding a new variant is easy; adding a new product type is not. That is the expression problem again. In modern code, dependency injection with configuration often replaces it: rather than a factory hierarchy, the container is configured to supply the right implementations. The pattern still applies when the family must be selected at runtime.
What is the Prototype pattern and where is it useful?
Prototype creates new objects by copying an existing instance rather than constructing from scratch. It is useful when construction is expensive — the object required a database query, a network call, or heavy computation to build — and you need many similar instances. Copying is cheaper than repeating that work. It also helps when the concrete type is not known at compile time. You have an object and want another like it, without knowing which subclass it is; asking it to clone itself solves that. The classic application is a registry of preconfigured prototypes cloned on demand, and object pools for expensive resources. The hazard is copy semantics. A shallow copy shares mutable state with the original, so modifying the copy modifies the source — a source of genuinely confusing bugs. A deep copy is correct but expensive and awkward with cycles and shared references. Java's Cloneable is widely regarded as a mistake — clone is protected, does not call constructors, and its contract is vague. The recommended alternative is a copy constructor or a static factory taking an instance, both of which are explicit about what is copied. For immutable objects the whole question disappears: sharing is safe.
How would you design an object pool and when is one justified?
An object pool reuses expensive-to-create objects rather than constructing and discarding them. Clients borrow, use and return. It is justified when creation is genuinely expensive relative to use — database connections, threads, large buffers. A connection involves a network round trip and authentication, so pooling is unambiguously worth it. It is not justified for ordinary objects. Modern JVM allocation is a pointer bump and young-generation collection is cheap, so pooling plain objects usually makes things slower while adding complexity and bugs. The design concerns: a maximum size, so the pool bounds resource use; a timeout on acquisition, so a client waits rather than blocking forever when exhausted; validation on borrow or return, since a pooled connection may have died while idle; and eviction of idle objects. The correctness hazard is state leaking between users. An object returned to the pool carrying data from its previous use will hand that data to the next borrower, which is a security issue when the object holds user context. Resetting on return — and not trusting callers to do it — is essential. And a pool must handle the case where a client never returns an object, or it slowly starves.
What is the difference between a static factory method and a constructor?
A static factory method is a static method that returns an instance, used instead of exposing a constructor. The advantages are concrete. It has a name, so Duration.ofSeconds and Duration.ofMinutes are distinguishable where two constructors taking a long would not be. It is not required to create a new object, so it can return a cached instance — Integer.valueOf returns cached instances for small values, and Boolean.valueOf never allocates. It can return a subtype, so the concrete class stays private and can change. And it can vary the returned type by argument. The disadvantages: a class with only private constructors cannot be subclassed, which is sometimes intended and sometimes not; and factory methods are less discoverable than constructors, which is why naming conventions like of, from, valueOf and getInstance matter. In practice, static factories are the better default for value types, and the Java standard library has moved decisively toward them — List.of, Map.entry, Instant.now. They are not the Factory Method pattern, which is about deferring the choice to a subclass. The naming overlap causes real confusion in interviews.
Why is Singleton often called an anti-pattern?
Because it combines two things that are individually reasonable and jointly harmful: guaranteeing one instance, and providing global access to it. The global access is the problem. Any code can reach the singleton without declaring it, so dependencies become invisible — a class's constructor tells you nothing about what it actually uses. That makes the coupling impossible to see and impossible to substitute. Testing suffers directly. You cannot inject a fake, tests share state through the singleton so they become order-dependent, and resetting between tests requires exposing a method that exists only for tests. It also tends to accumulate. A singleton is a convenient place to put things, so it grows into a god object. And the single-instance guarantee is often wrong later — you discover you need one per tenant, or one per test, and the design forbids it. The better approach is to have the container create exactly one instance and inject it. You get the single instance without the global access, dependencies are explicit in constructors, and tests can substitute freely. The legitimate uses are stateless utilities and genuine hardware singletons, where none of these costs apply.
How do you handle object creation that can fail?
Constructors cannot return a failure value, so you have three options. Throw from the constructor. This is correct when the arguments are invalid — a Money with a negative amount should not exist — and it enforces that the object is always valid. The caller handles an exception rather than checking a return value. Use a static factory returning an Optional or a Result type. This suits parsing and validation where failure is expected rather than exceptional, and it makes the failure visible in the signature. Optional.empty communicates "could not construct" without exception control flow. Use a builder that validates in build(). This handles cross-field validation cleanly and can accumulate multiple errors rather than failing on the first. The choice depends on whether failure is exceptional or routine. A configuration value that is malformed is exceptional — the program cannot proceed. User input that fails validation is routine, and should produce a list of errors rather than an exception per field. What to avoid is a constructor that succeeds and leaves the object in a half-initialised state with an isValid flag. That pushes the check onto every caller and they will forget.
What is dependency injection versus service location?
Dependency injection supplies a class's collaborators from outside, typically through the constructor. Service location has the class ask a registry for what it needs. The difference is visibility. With injection, the constructor signature lists everything the class depends on — you cannot construct it without providing them, so the dependencies are explicit and compile-time checked. With service location, the class calls Locator.get(PaymentGateway.class) somewhere in its body, so the dependency is invisible from outside and discoverable only by reading the implementation. That has practical consequences. A class using a service locator can be constructed successfully and then fail at runtime when it looks up something unregistered. Tests must configure the global locator rather than passing a fake, which reintroduces shared state and order dependence. Service location is sometimes justified — plugin architectures where dependencies genuinely are not known until runtime, or legacy code where changing constructors is impractical. But as a default it is worse, and it is frequently used because it is easier to retrofit than to fix constructors. Martin Fowler's original article covers both fairly; the consensus since has favoured injection.
What is the Adapter pattern and when do you reach for it?
An adapter converts one interface into another that a client expects, letting classes work together that otherwise could not. The typical case is integrating a third-party library whose interface does not match your domain. Rather than letting its types spread through your codebase, you define the interface you want and write an adapter implementing it in terms of the library. That containment is the real benefit. If you later swap the library, only the adapter changes. Without it, the library's types appear in hundreds of files and replacement is a rewrite. It is also how you make legacy code testable — wrap it behind an interface you control and substitute the adapter in tests. The distinction from similar patterns: a Facade simplifies a complex subsystem behind a smaller interface, whereas an Adapter converts between two interfaces of roughly equal complexity. A Decorator keeps the same interface while adding behaviour; an Adapter deliberately changes it. The caution is adapter proliferation — a layer of adapters over adapters. One adapter at a genuine boundary is valuable; adapters between your own internal layers usually indicate the layers were badly drawn.
What is the Decorator pattern and how does it differ from inheritance?
A decorator wraps an object, implements the same interface, and adds behaviour before or after delegating to the wrapped instance. The advantage over inheritance is composability at runtime. Java's I/O library is the standard example: a BufferedInputStream wrapping a GZIPInputStream wrapping a FileInputStream. With inheritance you would need a class for every combination — BufferedGzipFileInputStream — and the count explodes combinatorially. Decorators also let you add and remove behaviour dynamically, which a compile-time hierarchy cannot. It is the natural fit for cross-cutting concerns: logging, caching, retry, metrics, authorisation. Each is a decorator around the real implementation, and you compose the ones you want. The costs are real. A deeply decorated object produces long stack traces where the actual work is buried under wrappers. Identity is affected — the decorated object is not the same reference as the original, so equality and instanceof checks can surprise. And debugging requires knowing what is wrapped in what, which is decided at composition time and not visible from the type. Java I/O is also criticised for exactly this: powerful, but the API is hard to learn because the composition is not obvious.
What is the Proxy pattern and what are its variants?
A proxy stands in for another object, controlling access to it while presenting the same interface. The variants differ by purpose. A virtual proxy defers creating an expensive object until it is actually used — Hibernate's lazy loading is exactly this, returning a proxy that hits the database on first access. A protection proxy checks permissions before delegating. A remote proxy represents an object in another process, handling the network call transparently — the basis of RPC stubs. A caching proxy stores results. Structurally a proxy resembles a decorator: same interface, wraps the target, delegates. The difference is intent. A decorator adds behaviour the caller wants; a proxy controls access, and often the caller is unaware it exists at all. The practical significance in Java is that Spring uses proxies for @Transactional, @Cacheable and @Async. Understanding that explains the most common gotcha: a call from one method of a bean to another method of the same bean bypasses the proxy entirely, so the annotation does nothing. Self-invocation not triggering a transaction is a bug people hit repeatedly, and it makes no sense until you know a proxy is involved.
What is the Facade pattern?
A facade provides a simplified interface to a complex subsystem, hiding its internal structure from clients. The motivation is that a subsystem may be legitimately complex — many classes with intricate relationships — while most clients need only a few common operations. The facade exposes those, and clients avoid learning the whole thing. It reduces coupling: clients depend on the facade rather than on a dozen internal classes, so the internals can be restructured freely. The key point that distinguishes a good facade from a bad one is that it should not prevent access to the subsystem. Clients with unusual needs should still be able to use the underlying classes directly. A facade that hides everything forces the team to keep extending it for each new case, and it grows into a god class. In practice, a service layer over a set of repositories and domain objects is a facade. So is a client library wrapping an HTTP API. The design smell to watch for is a facade with fifty methods, which means it has stopped simplifying and has become a pass-through layer — pure indirection with no abstraction benefit.
What is the Composite pattern and where does it apply?
Composite lets you treat individual objects and compositions of objects uniformly, by having both implement the same interface. The structure is a tree: leaves do the actual work, and composites hold children and typically delegate to them. Because both implement the same interface, client code does not branch on whether it holds a leaf or a composite. The canonical applications are file systems, where a directory and a file both support size and delete; UI component trees, where a panel contains widgets and is itself a widget; and expression trees, where an operation node holds operands that may themselves be operations. The design tension is where to put the child-management methods. Putting add and remove on the shared interface makes clients uniform but means leaves must implement methods that make no sense, throwing or doing nothing — an Interface Segregation and Liskov violation. Putting them only on the composite is cleaner but forces clients to check the type. Gang of Four favoured uniformity; most modern practice favours safety and puts them on the composite. Which you choose depends on whether clients genuinely need to build trees generically.
What is the Bridge pattern and how is it different from Adapter?
Bridge separates an abstraction from its implementation so the two can vary independently, by holding the implementation as a field rather than inheriting it. The problem it solves is a combinatorial hierarchy. Shapes that can be drawn with different rendering engines: with inheritance you need CircleWithOpenGL, CircleWithSVG, SquareWithOpenGL and so on — the product of both dimensions. With a bridge, Shape holds a Renderer, and adding a shape or a renderer is one class rather than N. So the signal for Bridge is two independent axes of variation that would otherwise multiply. The difference from Adapter is intent and timing. Adapter is applied after the fact to make incompatible interfaces work together — usually with code you do not control. Bridge is designed upfront to keep two dimensions decoupled, and you control both sides. Structurally they look similar, which is why they are confused. The distinguishing question is whether you are fixing a mismatch or preventing a combinatorial explosion. JDBC is a reasonable example: the API is the abstraction and the driver is the implementation, and either can change independently.
What is the Flyweight pattern?
Flyweight reduces memory by sharing common state between many similar objects rather than duplicating it. The technique is to split state into intrinsic — shared and immutable, stored once in the flyweight — and extrinsic, which varies per use and is passed in as a parameter rather than stored. The classic example is a text editor with a million characters. Storing a full object per character with font, size and colour is enormous; storing one shared object per distinct character and passing position as an argument is not. Java's Integer cache is a flyweight: values from -128 to 127 are shared instances, which is why == comparison works for small integers and fails for large ones — a genuinely surprising behaviour that this pattern explains. String interning is another. The conditions for it to pay off are specific: many objects, most of their state shareable, and memory actually being a constraint. If those do not hold, you have added indirection and complexity for nothing. The correctness requirement is that shared state must be immutable. A mutable flyweight is shared mutable state, and modifying it affects every user — which is exactly the bug this pattern otherwise makes easy to write.
How do decorators, proxies and middleware relate?
They are the same structural idea applied at different levels, and noticing that is worth saying. All three wrap something, present the same interface or contract, do work before or after, and delegate. A decorator adds behaviour a caller wants. A proxy controls access, often transparently. Middleware in a web framework wraps request handling — authentication, logging, compression — with each layer calling the next. The chain-of-responsibility pattern is closely related: a sequence of handlers each deciding whether to handle or pass along. The practical value of recognising the shared shape is that the same design considerations apply. Order matters — authentication before authorisation, logging outside or inside error handling depending on what you want to see. Each layer should have one concern. And too many layers makes stack traces and debugging painful, which is a real cost in heavily-middlewared frameworks. It also explains why cross-cutting concerns are so often implemented this way: they are behaviour that applies to many operations without belonging to any of them, and wrapping is the mechanism that adds behaviour without modifying the thing being wrapped.
When is a wrapper class justified versus just using the underlying type?
A wrapper is justified when it adds meaning, constraints or a boundary — not merely when it adds a layer. The strongest case is a value object replacing a primitive: an EmailAddress rather than a String, a Money rather than a BigDecimal, a UserId rather than a long. The wrapper can validate on construction, so an invalid email cannot exist, and it makes the type system prevent mistakes — you cannot pass a UserId where an OrderId is expected, which a bare long allows. That is primitive obsession, and wrapping is the fix. The second case is a boundary: wrapping a third-party type so it does not spread through your code. The wrapper is not justified when it just forwards every method with no added behaviour, constraint or type safety. That is pure indirection — a reader must open two files to understand one operation, and nothing is gained. The test to apply: does the wrapper make an invalid state unrepresentable, or prevent a category of mistake, or isolate a dependency? If none of those, it is ceremony. Java's verbosity makes wrappers costly, which is why records helped so much here.
What is primitive obsession and how do you fix it?
Primitive obsession is representing domain concepts with primitives — a String for an email, a long for money in paise, a pair of doubles for a coordinate — instead of dedicated types. The costs accumulate. Validation is scattered, since every place that accepts an email string must check it, and one place will not. Type safety is lost: a method taking two longs for amount and accountId can be called with the arguments swapped, and the compiler is happy. Behaviour has nowhere to live, so formatting and arithmetic end up in utility classes. And the code does not express the domain. The fix is to introduce a value type. An Email that validates in its constructor cannot be invalid anywhere in the system — you validate once at the boundary and the type carries the guarantee. A Money that holds amount and currency makes currency mismatches a compile error rather than a silent bug. The objection is boilerplate, which records largely answer. The judgement is which concepts deserve it. Anything with validation rules, invariants, or a unit is a candidate. A name that is genuinely just text probably is not.
What is the Strategy pattern and when should you use it?
Strategy encapsulates interchangeable algorithms behind a common interface, so the algorithm can be selected and swapped independently of the code that uses it. The signal that you need it is a conditional selecting behaviour — a switch on payment type calling different calculation code, especially if the same switch appears in more than one place. Each branch becomes a strategy class, and the switch becomes a lookup. The benefits are that adding an algorithm means adding a class rather than editing existing logic, each strategy is independently testable, and the selection can be configured or injected rather than hardcoded. In modern Java a strategy is often just a lambda or a method reference, since a single-method interface is a functional interface. Comparator is a strategy; so is any Function passed to configure behaviour. That removes most of the ceremony the pattern originally carried. The judgement call is when not to. With two branches that will never grow, an if is clearer than two classes and a factory. The pattern earns its place when the set of algorithms is genuinely open, or when selection needs to be configurable at runtime.
What is the Observer pattern and what are its pitfalls?
Observer defines a one-to-many dependency: when one object changes state, all its registered dependents are notified automatically. It decouples the subject from its observers — the subject knows only that something implements the listener interface, not who or how many. The pitfalls are substantial and worth naming. Memory leaks: an observer that registers and never unregisters keeps the subject holding a reference to it, so it is never collected. This is one of the most common leak causes in long-lived applications, and it is why weak references and explicit deregistration matter. Ordering: observers are typically notified in registration order, which is an implementation detail nobody should depend on but somebody will. Exception handling: if one observer throws, do the rest still get notified? Naive implementations abort the loop, so one misbehaving listener silently breaks the others. Reentrancy: an observer that modifies the subject during notification can trigger a nested notification, causing infinite recursion or ConcurrentModificationException. And debugging is harder because control flow is indirect — you cannot see from the subject who will run. Event buses and reactive streams are the modern evolution, with the same trade-offs.
What is the State pattern and how does it compare to a state machine?
State lets an object alter its behaviour when its internal state changes, by delegating to a state object rather than branching on a state field. The motivation is that state-dependent behaviour otherwise produces the same switch statement in every method — a document that behaves differently when draft, published or archived ends up with three-way branching in save, delete, publish and render. With the pattern, each state is a class implementing the operations, and transitions are made by replacing the current state object. Adding a state means adding a class rather than editing every method. The relationship to a state machine is that this is one implementation of one. An explicit state machine — a transition table or a library — is the alternative, and it has an advantage: the transitions are data, so they can be visualised, validated for unreachable states, and changed without code. The pattern spreads transitions across classes, which makes the overall machine harder to see. If the state graph is complex, a table is clearer. Use the pattern when behaviour differs substantially per state; use a table when the graph itself is the complexity.
What is the Command pattern and what does it enable?
Command encapsulates a request as an object, with the receiver, the method and the parameters all bundled together. The immediate benefit is that an operation becomes a value you can pass around, store and manipulate. That enables several things a direct method call cannot. Undo and redo: if each command knows how to reverse itself, a stack of executed commands gives you undo. This is the classic motivation, and it is why editors use it. Queuing and scheduling: commands can be placed on a queue and executed later or elsewhere, which is what a job system is. Logging and replay: a persisted command log lets you reconstruct state by replaying — the basis of event sourcing. Macros: a composite command holding a list of others. The cost is a class per operation, which is significant boilerplate for simple cases. In modern Java a Runnable or a lambda is often a sufficient command, and the full pattern is needed only when you require undo or serialisation. The design point worth making is that the command should carry everything needed to execute, so it does not depend on ambient state that may have changed by execution time.
What is the Template Method pattern and what is its main weakness?
Template Method defines the skeleton of an algorithm in a base class, deferring specific steps to subclasses through abstract or overridable methods. The value is that the invariant structure lives in one place — the order of steps, the error handling, the setup and teardown — while variation is isolated to the hooks. A test framework running setup, test, teardown is a template method. The main weakness is that it relies on inheritance, with all the coupling that implies. Subclasses depend on the base class's implementation, not just its interface, so changing the template can break every subclass in ways the compiler will not catch. Java's single inheritance also means a subclass cannot participate in another hierarchy. It also inverts control in a way that can be hard to follow: reading a subclass, you cannot see when its hooks are called without reading the parent. The modern alternative is composition: pass the varying steps in as functions or as a strategy object. That gives the same structure with runtime flexibility, no inheritance, and easier testing. Template Method still fits frameworks where you control the base and want to constrain extension deliberately.
What is the Chain of Responsibility pattern?
A request is passed along a chain of handlers, each deciding whether to handle it or pass it to the next. The decoupling benefit is that the sender does not know which handler will process the request, or how many there are. Handlers can be added, removed and reordered without touching the sender. It is everywhere in practice: servlet filters, Spring Security's filter chain, Express and ASP.NET middleware, exception handling in most languages, and logging hierarchies where a level passes upward. The design decisions that matter: whether a handler that processes the request stops the chain or lets it continue — both are valid and the choice must be explicit; what happens when nothing handles it, which should be a defined fallback rather than silence; and how errors propagate. The pitfalls are that a long chain is hard to reason about, since determining what will happen requires knowing every handler and its order; and that a missing or misordered handler produces bugs that are hard to localise — authorisation running before authentication, for instance. Order is usually configuration, which means it is easy to get wrong and worth testing explicitly.
What is the Visitor pattern and why is it controversial?
Visitor separates an algorithm from the object structure it operates on. Each element accepts a visitor and calls back the method for its own type, which is double dispatch. The benefit is that you can add new operations over a structure without modifying the element classes. A compiler AST with type-checking, optimisation and code-generation visitors is the canonical case — each is a separate class, and the node types are untouched. The controversy is the trade-off it makes. Visitor is open to new operations and closed to new element types: adding a node type means changing the visitor interface and every implementation. That is the opposite of ordinary polymorphism, which is open to new types and closed to new operations. This is the expression problem, and you must choose which axis to favour. So Visitor is right when the structure is stable and operations proliferate, and wrong when new types arrive frequently. The other objections are practical: the double-dispatch mechanics are verbose and unintuitive, and the pattern spreads logic for one operation across many accept methods. Pattern matching and sealed types in modern Java address much of this more directly.
What is the Mediator pattern?
A mediator centralises communication between a set of objects, so they refer to the mediator rather than to each other. The motivation is that many-to-many relationships produce a mesh of dependencies. With n components each knowing the others, you have n² relationships and any change ripples. Routing through a mediator makes it n relationships. The classic example is a dialog where enabling one control depends on the state of several others. Without a mediator each control knows about the others; with one, each reports to the dialog and the dialog decides. The risk is that the mediator becomes a god object. All the interaction logic now lives in one class, and as the system grows that class grows with it. You have traded a distributed mess for a centralised one, which is more comprehensible but can become unmaintainable. The mitigation is to keep the mediator focused on coordination rather than letting business logic accumulate in it, and to split it when it grows. An event bus is a related idea with looser coupling — publishers do not know subscribers at all — at the cost of the indirection making flow harder to trace.
What is the Memento pattern?
Memento captures an object's internal state so it can be restored later, without exposing that internal representation. The key constraint is the encapsulation preservation. The memento holds the state, but only the originating object can read it — outsiders can hold and pass it around but cannot inspect or modify it. That is what distinguishes it from simply exposing getters and setters, which would let anyone construct an arbitrary state. The use case is undo, checkpointing, and transactional rollback. An editor snapshots the document before an operation; a game saves state; a wizard remembers the previous step. The practical issues are memory and cost. Snapshotting a large object graph on every operation is expensive, so real implementations usually store deltas rather than full states, or snapshot periodically and replay from there — which is the same idea as database checkpointing with a write-ahead log. Java has no language support for the encapsulation part, so it is usually approximated with a private nested class or a package-private interface. The pattern pairs naturally with Command: the command performs the action, the memento restores what it changed.
What is the Iterator pattern and why does it matter?
Iterator provides a way to traverse a collection without exposing its internal structure. It matters because it decouples traversal from representation. Client code that iterates works identically over an array-backed list, a linked list, a tree or a database cursor — the collection's internals are irrelevant. That is why for-each works uniformly in Java, and why a method accepting Iterable can be given anything. The design points worth knowing: an external iterator, where the client controls advancement, gives more flexibility — you can stop early or interleave two iterations. An internal iterator, where you pass a function to be applied, is simpler but less flexible. Java's Iterator is external; forEach is internal. Fail-fast behaviour is a related detail. Java collections detect structural modification during iteration and throw ConcurrentModificationException, which is a best-effort bug detector rather than a guarantee. The correct fixes are to use the iterator's own remove, to collect changes and apply after, or to use a concurrent collection. Streams are an evolution of the internal iterator, adding laziness and composition on top.
How do you choose between patterns that look similar?
By intent, because several patterns share almost identical structure and differ only in purpose. Decorator, Proxy and Adapter all wrap an object and delegate. Decorator adds behaviour the caller wants; Proxy controls access, usually transparently; Adapter converts between mismatched interfaces. Same shape, three different problems. Strategy and State both delegate to an interchangeable object. Strategy is chosen by the client and does not change on its own; State is changed by the object itself as part of its lifecycle. Bridge and Adapter both decouple two hierarchies. Bridge is designed upfront to prevent combinatorial explosion; Adapter is retrofitted to fix an incompatibility. The practical advice is to describe the problem before naming the pattern. If you say "I need to add logging without changing the class", the answer is a decorator regardless of what you call it. Starting from the pattern name and trying to fit the problem to it is how people end up with an Abstract Factory for two classes. And it is entirely fine to implement the right structure without naming it. The names are for communication, not for permission.
When are design patterns the wrong answer?
When the problem does not have the shape the pattern addresses, which is more often than pattern enthusiasm suggests. The common failures: applying a pattern because it is familiar rather than because it fits; using an Abstract Factory where a constructor would do; wrapping a single implementation in a Strategy interface with no second strategy in sight; building an observer mechanism for one listener. Each adds indirection, and indirection has a cost — a reader must traverse more files to understand less. Patterns also encode assumptions from a specific era. Several Gang of Four patterns exist partly to work around limitations of C++ and early Java. Strategy is largely a lambda now. Iterator is language syntax. Singleton is usually a DI scope. Command is often a Runnable. The pattern names remain useful for discussion, but the elaborate class structures frequently do not. The honest framing for an interview: patterns are vocabulary for describing solutions, not a catalogue to apply. The right sequence is to solve the problem simply, notice when the solution is straining, and then recognise which known shape fits — rather than choosing a pattern first and shaping the problem to it.
How do you design a class to be thread-safe?
In order of preference: make it immutable, confine it to one thread, or synchronise access. Immutability is the strongest answer because it removes the problem — an object that cannot change cannot be seen in an inconsistent state, needs no locks, and can be shared freely. Make fields final, do not expose mutable internals, and return copies or immutable views from accessors. Confinement means the object is only ever touched by one thread, so no synchronisation is needed. Stack-confined locals and ThreadLocal both qualify. When you must have shared mutable state, synchronise all access to it — reads as well as writes, since a read without synchronisation may see a stale or partially-constructed value. Use a private lock object rather than synchronising on this, so callers cannot interfere with your locking. Document the policy. A class that is thread-safe for some methods and not others, with no documentation, is worse than one that is clearly not thread-safe. And prefer the platform's concurrent collections and atomics over hand-rolled locking, because they are correct and usually faster than what you would write.
What is the difference between thread-safe and reentrant?
Thread-safe means correct when called concurrently from multiple threads. Reentrant means correct when called again before a previous invocation has completed — typically via recursion or a callback. They are independent properties, and confusing them causes real bugs. A method can be thread-safe but not reentrant. A method that acquires a non-reentrant lock is safe across threads but deadlocks if it calls itself. Java's intrinsic locks are reentrant, which is why synchronized recursion works; POSIX mutexes by default are not. A method can be reentrant but not thread-safe. A pure function using only its parameters and locals is reentrant, but if it touches unsynchronised shared state it is not thread-safe. The practical relevance is callbacks. If you invoke a listener while holding a lock, and that listener calls back into your object, you need reentrancy — and if it acquires locks in a different order, you have a deadlock. That is the concrete reason for the rule about not calling unknown code while holding a lock: you cannot reason about reentrancy or lock ordering for code you have never seen.
How would you design a thread-safe cache?
The naive answer is a synchronized map, which is correct but serialises every access and scales badly. Use ConcurrentHashMap instead, which partitions internally so concurrent reads and most writes proceed without contention. The crucial design point is the check-then-act race. Code that checks whether a key is present and then computes and puts it is not atomic — two threads can both miss and both compute, which is wasteful if computation is expensive and incorrect if it has side effects. computeIfAbsent solves this: the computation happens once per key, with other threads for that key blocking until it completes. That is request coalescing built into the map, and it is the single most important thing to get right. Beyond correctness: bound the size, or the cache is a memory leak. Choose an eviction policy — LRU is the usual default, and LinkedHashMap with removeEldestEntry gives it cheaply, though not concurrently. Decide on expiry, since stale data is often worse than a miss. In practice, use Caffeine rather than building it. It handles eviction, expiry, refresh, coalescing and statistics, and its eviction algorithm outperforms plain LRU.
What is safe publication and why does it matter?
Safe publication means making an object visible to other threads in a way that guarantees they see it fully constructed. Without it, a thread can observe a reference to an object whose constructor has not finished — seeing default values for fields that the constructor set. This is not theoretical; it is permitted by the memory model because the compiler and CPU may reorder the constructor's writes relative to the reference assignment. The safe ways to publish: initialise from a static initialiser, store into a volatile field or an AtomicReference, store into a final field of a properly constructed object, or store into a field guarded by a lock that readers also acquire. The final field guarantee is the one worth knowing: if an object has only final fields, correctly assigned in the constructor, and the reference does not escape during construction, then any thread seeing the reference sees the fully initialised fields. That is why immutable objects can be shared without synchronisation. The common mistake is publishing this from a constructor — registering a listener or starting a thread — which lets another thread see a partially built object. Use a factory method that constructs first and publishes after.
How do you avoid deadlock when a design needs multiple locks?
The primary technique is a global lock ordering. If every thread acquires locks in the same order, a cycle cannot form, and therefore neither can a deadlock. For fixed locks that ordering can be a documented convention. For dynamic locks — transferring between two accounts, where either could be first — order by something stable such as the account identifier, or by System.identityHashCode with a tie-breaker lock for the rare collision. The second technique is timed acquisition. tryLock with a timeout means a thread that cannot get everything releases what it holds and retries, which breaks hold-and-wait. Add jitter or two threads will retry in lockstep and livelock. Beyond mechanism, the design advice is to need fewer locks. Reduce the scope of each lock, avoid holding one while calling into code you do not control, and prefer immutable objects and concurrent collections that handle their own synchronisation. The strongest structural answer is often to avoid shared mutable state entirely — give each thread its own data and combine results, or serialise access through a single owner such as an actor or a queue consumer. No shared state, no lock ordering problem.
What is the producer-consumer pattern and how do you size the queue?
Producers put work on a shared queue and consumers take from it, decoupling the rate of production from the rate of consumption. The benefits are that producers are not blocked by slow consumers, consumers can be scaled independently, and bursts are absorbed by the queue. Queue sizing is the interesting decision. An unbounded queue never blocks producers, which sounds good and is dangerous: if consumers cannot keep up, the queue grows until you run out of memory. A throughput problem becomes an OutOfMemoryError, and this is exactly why Executors.newFixedThreadPool is risky under sustained overload. A bounded queue applies back-pressure — producers block or are rejected when it is full — which propagates the slowdown to where it can be handled. That is almost always what you want in a service. The size should be large enough to absorb normal bursts and small enough that queued work is still relevant when it is processed. A queue holding ten minutes of work in a system with a one-second latency target is just a place where requests go to time out. Decide the rejection policy explicitly: block, drop oldest, drop newest, or fail fast.
How do you design for idempotency in a concurrent system?
Idempotency means an operation applied more than once has the same effect as applying it once — which is what makes retries safe when you cannot tell whether the first attempt succeeded. The standard mechanism is a deduplication key. The caller supplies a unique identifier per logical operation; the server records it atomically with the effect, and a repeat with the same key returns the original result rather than acting again. The atomicity is the part that is easy to get wrong. Checking whether the key exists and then performing the operation is a race — two concurrent retries can both pass the check. The insert of the key and the effect must be in one transaction, or the key must be inserted first with a unique constraint so the second attempt fails cleanly. Designing operations to be naturally idempotent is better where possible. Setting a value is idempotent; incrementing is not. Assigning a state is; appending is not. Reframing an operation as an assignment often removes the need for keys entirely. And decide how long keys are retained, since storing them forever is a growth problem and expiring them too soon reopens the window.
When should a design use asynchronous processing instead of synchronous?
When the caller does not need the result to proceed, or when the work is too slow to hold a request open. The test is whether the outcome is required for the response. Sending a confirmation email after an order is placed is not — the order succeeded regardless, and making the customer wait on an email provider couples your availability to theirs. Charging their card is required, so it stays synchronous. Moving work asynchronous improves latency and decouples failure: a downstream outage delays processing rather than failing the request. The costs are real and often underestimated. You need a queue or scheduler to operate. Failure handling becomes your problem — retries, dead-letter queues, and alerting on work that never completes. The caller cannot be told about failure directly, so you need a way to surface it. And you have introduced eventual consistency, so the client may read state that does not yet reflect its own write, which is confusing unless designed for. The pattern that avoids the worst failure mode is the transactional outbox: write the work item in the same transaction as the state change, so you cannot commit one without the other.
How would you design a parking lot system?
Start with the entities: ParkingLot containing Levels, each with ParkingSpots of different sizes; Vehicle with subtypes; Ticket recording entry; and a pricing strategy. The interesting decisions are where interviewers probe. Spot allocation: a naive scan is O(n). Keeping a queue of free spots per size gives O(1) allocation, at the cost of maintaining it on entry and exit. Vehicle-to-spot fitting: a motorcycle can use a car spot but not vice versa. Model that as an explicit compatibility rule rather than scattering conditionals — a spot type that knows which vehicle types it accepts. Pricing: hourly, daily, different rates by vehicle type or time of day. This is a strategy — a PricingStrategy interface so a new scheme is a new class rather than an edit to a switch. Concurrency: two cars arriving simultaneously must not be assigned the same spot. Allocation needs to be atomic — a lock or a concurrent structure with compare-and-set. The extensions to anticipate: reserved spots, electric charging, multiple entrances, and finding your car. Mentioning that you would keep the spot-assignment policy behind an interface for exactly this reason is the right instinct.
How would you design an elevator system?
The entities are straightforward — Elevator, Floor, Request, and a Controller — so the design conversation is really about the scheduling algorithm and the state model. Each elevator is a state machine: idle, moving up, moving down, doors open. Transitions are constrained, and modelling them explicitly prevents illegal states like moving with doors open. Requests come in two kinds, and conflating them is the common mistake. An external request is a floor plus a direction — someone pressing up on floor three. An internal request is a destination from inside the car. They are scheduled differently: an external up-request can only be served by a car travelling up. The scheduling algorithm is the substance. FCFS is simple and terrible. The SCAN or elevator algorithm — continue in the current direction serving all requests, then reverse — is the standard, because it bounds waiting time and avoids the thrashing of always serving the nearest request. With multiple cars, the controller assigns each request to the best car, scoring by distance, current direction and load. Keep the scheduler behind an interface. It is the part most likely to change, and interviewers usually ask you to swap it.
How would you design a rate limiter?
Define the interface first: allow(key) returning a decision, where the key identifies whoever is being limited. The algorithm is the main design choice. Fixed window is simplest — count per interval — but allows a burst of double the limit across a boundary. Sliding window log is exact but stores a timestamp per request. Sliding window counter interpolates between two windows and is a good compromise. Token bucket allows controlled bursts while enforcing an average rate, and is usually the right default. Model the algorithm as a strategy so it can be swapped, because the requirement usually changes. The distributed question comes next. Per-instance limiting is simple but the effective limit multiplies by instance count. A shared store — Redis with an atomic Lua script — gives a global limit at the cost of a round trip per request. The script must be atomic, or concurrent requests race on the check-then-increment. The details worth raising: what happens when the store is unavailable — fail open, accepting the risk, or fail closed, accepting the outage? And returning the remaining quota so clients can self-pace rather than discovering the limit by hitting it.
How would you design an LRU cache?
The requirement is O(1) get and put with eviction of the least recently used entry, which dictates the structure. A hash map alone gives O(1) lookup but cannot tell you what is least recently used. A list alone gives ordering but O(n) lookup. The answer is both: a hash map from key to node, and a doubly linked list ordered by recency. On get, look up the node in the map and move it to the head of the list. On put, insert at the head; if over capacity, remove the tail and delete its key from the map. The doubly linked list is required rather than singly linked, because moving a node to the head needs O(1) removal from its current position, which requires a reference to the previous node. The implementation details that catch people: sentinel head and tail nodes remove all the null-checking at the boundaries, and the node must store its key so that evicting the tail can remove the right map entry. In Java, LinkedHashMap with accessOrder true and removeEldestEntry overridden gives this in a few lines — worth mentioning, then implementing manually since that is what is being asked. For thread safety, guard both structures with one lock; they must move together.
How would you design a notification system?
The core abstraction is a NotificationChannel interface with implementations for email, SMS, push and in-app. That is a strategy, and it means adding WhatsApp is a new class rather than an edit. A Notification carries the recipient, content and metadata. A template system separates content from data, so the same notification renders per channel and per language. The routing decision — which channels to use for a given event and user — belongs in a preferences service, not scattered through senders. Users opt in and out per category and channel, and that must be respected centrally or you will eventually send something to someone who opted out. Delivery must be asynchronous. Sending inline couples your request latency and availability to an email provider. Queue the notification and let workers deliver. The reliability concerns are where the design earns marks: retries with backoff for transient provider failures, a dead-letter queue for permanent ones, deduplication so a retry does not send twice, and rate limiting per user so a bug cannot send a thousand emails. And a delivery status record, because "did the user get it?" is always the first question in support.
How would you design a URL shortener at the class level?
Two operations — shorten and resolve — behind a service interface, with the interesting design in code generation and storage. Code generation is a strategy. Base62 encoding of a monotonically increasing counter is collision-free by construction and produces short codes; hashing the URL and truncating is stateless but requires collision handling. Keep it behind an interface, because the choice depends on scale and interviewers ask you to change it. The counter is the scaling constraint. A single database sequence is a bottleneck and a single point of failure, so a distributed ID generator or per-instance ranges preallocated in blocks is the usual answer. Storage is a simple key-value mapping from code to URL, plus metadata: creation time, expiry, owner, and click count. The read path dominates by a large factor, so a cache in front is essential. The design decisions worth raising: custom aliases need a uniqueness check and a reserved-word list; expiry needs a cleanup strategy; and click counting should be asynchronous, since incrementing a counter synchronously on every redirect puts a write on the hot path of a read-heavy system. Redirect should be 302, not 301, or analytics stop working.
How would you design a library management system?
The entities are Book, BookCopy, Member, Loan and Reservation — and the distinction between Book and BookCopy is the modelling point that separates good answers from poor ones. A Book is the bibliographic record: title, author, ISBN. A BookCopy is a physical item with its own barcode and condition. Members borrow copies, not books. Conflating them makes it impossible to track which copy is where, or to have five copies of one title. A Loan links a member to a copy with issue and due dates. A Reservation queues members for a title when all copies are out, and needs a policy for what happens when one is returned — notify the first in queue and hold it for a period. The rules that need a home: borrowing limits per member type, loan durations, renewal eligibility, and fine calculation. These change and vary, so keep them in policy objects rather than in if-statements inside the loan service. The concurrency case interviewers probe: two members reserving the last copy simultaneously. Allocation must be atomic. And fines are a scheduled calculation, not a field — computed from the due date and the return date rather than stored and updated.
How would you design a movie ticket booking system?
Entities: Movie, Theatre, Screen, Show, Seat and Booking. A Show is a movie playing on a screen at a time, and seats are per-screen while availability is per-show — that separation matters, because the same physical seat is free for one show and taken for another. The hard part, and the reason this problem is asked, is seat locking. Without it, two users select the same seat and both proceed to payment, and one of them fails after paying. The standard solution is a temporary hold: when a user selects seats, they are locked for a few minutes with an expiry. Payment converts the hold to a booking; timeout releases it. The hold must be acquired atomically — a conditional update or an insert with a unique constraint on show and seat — or the race persists. Expiry needs care: a background sweeper, or lazily treating expired holds as free when checked, with the latter avoiding a scheduled job. The other decisions: pricing as a strategy, since it varies by seat class, time and day; and idempotency on the booking creation, so a retried payment callback does not double-book. Cancellation and refund policy is the usual follow-up.
How would you design a chess game?
Board holding an 8x8 grid of Squares, Piece as an abstract type with concrete subclasses, Player, Move and Game managing turns and state. The central design decision is where move validation lives. Putting it in a switch on piece type inside the board is the wrong answer and the one that gets given. Each Piece should know its own movement rules — a polymorphic isValidMove or getPossibleMoves. Adding a variant piece is then a new class. But piece-level rules are not sufficient, and saying so is what shows depth. Some rules are global: you may not make a move that leaves your own king in check. Castling depends on whether the king and rook have moved and whether intervening squares are attacked. En passant depends on the immediately preceding move. Promotion depends on reaching the last rank. So you need both piece-level movement and a game-level validator that filters candidate moves by legality. The Move object should carry enough to be undone, which gives you takeback and is needed for the check test — make the move, test for check, unmake it. Game state needs draw detection: stalemate, threefold repetition, fifty-move rule.
How would you design a food delivery system at the class level?
The entities span three parties: Customer, Restaurant with a Menu, Order, DeliveryPartner, and Payment. The Order is the centre and is best modelled as a state machine — placed, accepted, preparing, ready, picked up, delivered, cancelled — with explicit legal transitions. Doing this as a status string plus scattered if-checks is how illegal states appear in production, such as an order delivered before it was accepted. The interesting design areas: partner assignment, which is a strategy scored by distance, current load and rating, and which interviewers always ask you to change; pricing, which composes item cost, delivery fee, surge, taxes and discounts, and is best expressed as a chain of applied rules rather than one formula. Cancellation policy varies by state — free before acceptance, partial after preparation begins — which is more state-dependent behaviour. The concurrency point is assignment: two orders must not be given to the same partner simultaneously, so assignment needs to be atomic. And the events matter: each state transition notifies interested parties, which is naturally an observer or an event publication rather than direct calls from the order to three services.
How would you design a logging framework?
Logger as the client-facing interface, with level methods; LogLevel as an ordered enum; LogRecord carrying message, level, timestamp, thread and context; Appender for destinations; Formatter for rendering; and a Filter for deciding what is emitted. The design points: levels are checked before formatting, so an expensive message is not constructed when the level is disabled. That is why lazy evaluation matters — passing a supplier or using parameterised messages rather than string concatenation at the call site, which is the single most common performance mistake with logging. Appenders are a strategy and a composite: one logger writes to several destinations, and adding one is a new class. Logger hierarchy by name, with levels inherited from parent to child unless overridden, is what gives per-package configuration. Asynchronous appending matters for throughput — a bounded queue with a background writer means logging does not block the request thread on disk I/O. The trade is that a crash may lose buffered records, which is why some appenders flush synchronously at error level. And mapped diagnostic context, carrying a request or trace ID via ThreadLocal, is what makes logs correlatable — and the thing most often missed across async boundaries.
How do you approach a machine coding round in the time available?
Clarify scope first, and narrow it aggressively. Ask which features are in scope and state explicitly what you are leaving out — persistence, authentication, UI. Interviewers almost always accept a narrower scope, and candidates who try to build everything finish nothing. Then identify the entities and their relationships out loud, and get agreement before writing code. Five minutes here saves twenty later. Write the interfaces before the implementations. That forces the design decisions to the front and gives the interviewer something to react to while it is still cheap to change. Build the happy path end to end before adding variations. A working narrow system beats a half-built broad one, and it gives you something to demonstrate. Use in-memory collections rather than a database, and say that you are doing so behind a repository interface so it could be swapped. Leave the extension points where you expect the follow-up questions — pricing, allocation, scheduling — behind interfaces, because the interviewer will ask you to change exactly those. And write a small main or test that exercises it. Code that has never run is a liability, and running it is what proves the design works.
How do you decide where a piece of behaviour belongs?
Put behaviour with the data it operates on. That single heuristic resolves most cases. If a method spends its time calling getters on another object, it is on the wrong class — that is feature envy, and the behaviour should move to the object whose data it uses. A method computing an order total by fetching every line item's price and quantity belongs on Order, not on a service. When behaviour genuinely spans two objects and belongs to neither, that is when a service or domain service is justified — transferring money between accounts is the classic example, since it belongs to neither account. The test for whether you got it right: does the method need more of its own class's data, or another's? And could you make the other class's fields private without breaking this method? The common failure is defaulting everything to a service layer because that is where logic conventionally goes. That produces anemic objects and services that grow without limit. The opposite failure is putting infrastructure concerns on domain objects — an Order that knows how to save itself couples the domain to persistence, which is why repositories exist.
What is an aggregate and why does it matter?
An aggregate is a cluster of objects treated as one unit for changes, with a single entry point called the aggregate root. Outside code holds a reference only to the root and goes through it to reach anything inside. The purpose is enforcing invariants. If an Order must never exceed a total value, and line items can be modified independently, the rule cannot be enforced. Making Order the root and requiring all line item changes to go through it means every change passes the check. It also defines a transactional boundary: an aggregate is loaded, changed and saved as a unit, and consistency is guaranteed within it. The design guidance that follows is to keep aggregates small. A large aggregate means loading and locking more than necessary, and it creates contention when many users modify different parts. Referencing other aggregates by identifier rather than by object reference keeps them separate. Consistency across aggregates is then eventual rather than immediate, which is a deliberate trade — you accept a window of inconsistency in exchange for smaller transactions and better concurrency. The practical signal you got the boundary wrong is a transaction that locks half the database.
How do you model something that changes over time?
The question is whether you need the current state, the history, or both. If only the current state matters, mutate in place — the simplest option and usually correct. If history matters, do not overwrite. Options include a separate audit table recording changes, temporal versioning where each row has a validity period and the current one has an open end date, or event sourcing where you store the sequence of changes and derive state by replaying them. Event sourcing gives complete history and the ability to reconstruct state at any past moment, which is powerful for auditing and for answering questions nobody anticipated. The costs are substantial: reading current state requires replay or a maintained projection, schema evolution of old events is genuinely hard, and the model is unfamiliar to most teams. Temporal tables are a much lighter middle ground and cover most real requirements. The modelling point that matters regardless: distinguish the time something happened from the time it was recorded. They differ — a correction recorded today about last week's transaction — and conflating them makes historical queries wrong. That is bi-temporal modelling, and it is what financial and insurance systems need.
How do you avoid a boolean parameter problem?
A boolean parameter at a call site is unreadable — process(order, true) tells the reader nothing, and they must open the method to find out what true means. Worse, a boolean parameter usually means the method does two things and branches on which. That is a Single Responsibility violation hiding in a signature. The fixes, in order of preference. Split the method: processImmediately and processDeferred are self-documenting and each does one thing. This is right when the branch splits the behaviour substantially. Use an enum instead of a boolean: process(order, Mode.DEFERRED) reads correctly and extends to a third mode without changing the signature — which a boolean cannot. Use a named parameter object when there are several flags, which also stops callers passing them in the wrong order. The worst case is multiple booleans — process(order, true, false, true) is unreadable and the arguments are trivially transposable with no compiler error. The exception is when the boolean is genuinely a domain value rather than a mode switch: setActive(true) is fine, because true is the value being set rather than a hidden branch.
How do you model optional or missing values?
Use Optional as a return type where absence is a legitimate outcome, and make it explicit in the signature so callers cannot ignore it. The value is that a method returning Optional<User> tells the caller absence is possible, whereas one returning User that sometimes returns null does not — and the caller finds out at runtime. The conventions worth following: do not use Optional for fields, since it is not serialisable and adds an allocation per object. Do not use it for parameters — an overload is clearer than forcing callers to wrap. And never return null from a method declared to return Optional, which happens more than it should. Use the functional methods — map, filter, orElseGet — rather than isPresent followed by get, which is just a null check with more typing. For domain modelling, there is often a better answer than Optional: the null object pattern, where a NoDiscount object implements the interface with neutral behaviour, removes the check entirely. Or restructure so the absent case is a different type — a sealed hierarchy of Found and NotFound — which forces the caller to handle both. The deeper goal is making invalid states unrepresentable.
What does "make illegal states unrepresentable" mean?
Design your types so that invalid combinations cannot be constructed, rather than constructing them and validating afterwards. The contrast: a class with a status string and a cancellationReason field allows a completed order with a cancellation reason — a state that should not exist. You then need validation everywhere and a rule nobody documented. The alternative is to model states as distinct types, so the reason field exists only on the cancelled variant. A sealed interface with records for each state does this in modern Java, and pattern matching forces callers to handle every case. Other applications: a NonEmptyList type so code that requires at least one element cannot receive zero. A value object that validates in its constructor, so an invalid Email cannot exist anywhere. Replacing a pair of nullable start and end dates with a DateRange that validates ordering. The benefit is that validation happens once at construction rather than defensively at every use, and the compiler enforces what documentation otherwise merely requests. The limit is that Java's type system cannot express everything, and pushing too far produces types more complex than the rules they encode. The judgement is which invariants are worth the type.
How do you decide between an enum and a class hierarchy?
Use an enum when the set of values is fixed, small, and the variation is data rather than behaviour. Use a hierarchy when each variant has substantially different behaviour or its own data. An enum is simpler, gives exhaustive switching, works as a map key, and serialises predictably. Java enums can also hold fields and abstract methods per constant, which covers a surprising amount — an enum where each constant implements a calculation is a perfectly good strategy for a closed set. The hierarchy wins when variants carry different data. If a CreditCardPayment needs a card number and a BankTransfer needs an account, an enum cannot express that without nullable fields for everything. It also wins when the set is open — plugins, or types added by configuration — since adding an enum constant requires recompiling and redeploying. The modern middle ground is sealed interfaces with records, which gives exhaustive matching like an enum plus per-variant data like a hierarchy. That is usually the right answer now for a closed set with differing shapes. The smell to avoid either way is a switch on an enum appearing in several places, which means the behaviour should live with the variant.
How do you keep domain logic independent of frameworks and databases?
Define the interfaces the domain needs, own them in the domain, and implement them in the infrastructure layer — so dependencies point inward. Concretely: the domain declares an OrderRepository interface expressing what it needs. A JpaOrderRepository in the infrastructure layer implements it. The domain has no compile-time dependency on JPA, the database, or Spring. That is dependency inversion applied structurally, and it is what hexagonal or ports-and-adapters architecture describes. The practical benefits: domain logic can be tested without a database or a container, which makes tests fast enough to run constantly. Infrastructure can be replaced. And the domain reads as domain logic rather than as persistence code. The cost is mapping. Domain objects and persistence entities are separate types, so you write translation between them. Many teams skip this and annotate domain objects directly with JPA annotations, which is pragmatic but couples the domain to the ORM — and the ORM then shapes your model, which is where anemic domains often come from. The judgement is scale. For a small CRUD service the mapping is overhead. For a system with real domain complexity and a long life, it repays.
What code smells do you look for first?
Long method and large class, because they usually indicate a missing abstraction rather than being problems in themselves. Duplicated code — but only when it represents the same knowledge, since coincidental similarity should be left alone. Long parameter lists, which usually mean a missing parameter object or a class doing too much. Feature envy: a method using another object's data more than its own, which means the behaviour is on the wrong class. Switch or if-else chains on a type, repeated in more than one place — the strongest indicator of missing polymorphism. Primitive obsession, where domain concepts are represented as strings and numbers so validation is scattered and type safety is absent. Shotgun surgery: one conceptual change requiring edits in many files, which means a responsibility is spread too thin. And names that describe nothing — Manager, Helper, Util, Processor — which usually mark a class that accumulated whatever nobody knew where to put. The meta-point worth making is that a smell is a hint, not a defect. Each has legitimate exceptions, and the value is in prompting the question, not in mandating a change.
How do you refactor safely?
Have tests first. Refactoring by definition preserves behaviour, and without tests you cannot know whether you did. If the code is untestable, the first step is a characterisation test — one that captures current behaviour, bugs included, so you can detect change. Then work in small steps with the code compiling and passing between each. A refactoring that takes four hours before it compiles is a rewrite, and rewrites are where behaviour quietly changes. Use automated refactorings where the IDE offers them — extract method, rename, move — because they are mechanically correct in ways hand-editing is not. Separate refactoring commits from behaviour-change commits. A commit that both restructures and fixes a bug is very hard to review, and if it breaks something you cannot tell which half was responsible. For legacy code without seams, the Michael Feathers technique applies: find the smallest change that introduces a testable seam — extract a method, introduce an interface, parameterise a constructor — then test, then refactor properly. And know when to stop. Refactoring is not free, and code that works and rarely changes may not be worth improving.
How long should a method be?
Short enough that its purpose is obvious without scrolling — but the line count is a symptom rather than the rule. The useful criterion is a single level of abstraction. A method that mixes high-level orchestration with low-level detail is hard to read because you switch levels while following it. Extracting the detail into named methods lets the top level read as a summary. That naturally produces short methods without targeting a number. The counter-argument worth acknowledging: extracting aggressively produces many tiny methods, and following a single operation means jumping between them. A method called once, whose name adds nothing over its body, is not obviously an improvement. So the test is whether the extracted method has a name that says something the body does not make immediately obvious. If you can name it well, extract it — the name is documentation and the call site reads better. If the best name you can find is a restatement of the code, leave it inline. A long method of genuinely sequential steps at one level of abstraction — a parser, a state machine — is sometimes clearer than the same logic scattered.
What makes a good name?
It says what the thing is or does, in the vocabulary of the domain, without requiring the reader to look at the implementation. The properties that matter: reveal intent rather than mechanism — dailyRate rather than d or tempValue. Use domain language, so the code reads like the problem and conversations with domain experts map to code. Be consistent — if it is a customer everywhere, do not call it a client in one place. Avoid encoding types in names, since the compiler already knows. Name length should scale with scope: a loop index can be i, a field spanning a class should be fully descriptive. Boolean names should read as predicates — isActive, hasPermission, canCancel. Method names should say what, not how, so the implementation can change without the name becoming a lie. getCachedUser is a worse name than getUser, because caching is an implementation detail. The strongest signal that a name is wrong is difficulty finding one. If you cannot name a class precisely, you have not identified its responsibility, and the naming problem is a design problem wearing a disguise.
When should you write a comment?
When the code cannot express something — which is less often than comments appear, and more often than "self-documenting code" advocates admit. Good comments explain why, not what. Why this algorithm rather than the obvious one, why this apparently redundant check exists, why a workaround is here and what it is working around, what a magic constant was derived from. That information genuinely cannot live in code. Also good: documenting a public API contract — preconditions, thrown exceptions, thread safety — and warning about non-obvious consequences. Bad comments restate the code, which adds noise and drifts out of date. A comment saying "increment the counter" above counter++ is worse than nothing, because it must be maintained and will eventually be wrong. Commented-out code should be deleted; version control remembers it. The key insight is that a comment explaining what code does is often a failed extraction — the right response is usually to extract the block into a well-named method, where the name replaces the comment and cannot go stale. But treating every comment as a failure is dogma. Some context simply has no home in code.
How do you handle exceptions well in a design?
Throw for exceptional conditions, not for expected outcomes. A user not found in a lookup is usually expected — returning Optional is better than an exception, which is both slower and turns a normal path into control flow via stack unwinding. Catch only where you can do something useful. A catch block that logs and rethrows adds noise and duplicate log entries; one that swallows the exception hides failures. If you cannot handle it, let it propagate to a layer that can. Do not catch generic Exception in business logic, since it captures programming errors alongside expected failures and treats them identically. Preserve the cause when wrapping. Losing the original stack trace makes diagnosis dramatically harder, and it is one of the most common mistakes. Wrap low-level exceptions in domain terms at boundaries — an SQLException should not escape your repository, because callers should not know you use SQL. On checked versus unchecked: checked exceptions force handling but pollute signatures and are frequently swallowed to satisfy the compiler. Most modern Java favours unchecked for anything the caller cannot meaningfully recover from. And have a single top-level handler mapping exceptions to responses, so no endpoint invents its own.
What is technical debt and how do you decide when to pay it down?
Technical debt is the accumulated cost of shortcuts — code that works but makes future change more expensive. The metaphor is apt because it accrues interest: every additional feature built on a poor foundation costs more. The useful distinction is deliberate versus inadvertent. Deliberate debt is a conscious trade — ship now, fix later — and is often correct. Inadvertent debt comes from not knowing better, and is discovered rather than chosen. Deciding when to pay involves the interest rate rather than the principal. Ugly code in a module nobody touches costs nothing; moderately messy code in the area you change weekly costs constantly. So prioritise by change frequency, not by how bad the code looks. The practical approach is opportunistic refactoring — improve the area you are already working in, as part of the work, rather than seeking a separate refactoring project. Those projects are hard to justify, hard to scope, and get cancelled. For debt too large for that, make the cost visible in terms the business understands: this is why the feature took three weeks rather than one. Arguing for cleanliness on aesthetic grounds rarely wins.
How do you approach adding a feature to code you do not understand?
Read before writing, and get a safety net before changing anything. Start by finding the entry point for the behaviour you are changing and tracing it, ideally with a debugger, since reading alone gives you a plausible story rather than the actual one. Then establish whether tests exist for the area. If they do, run them and read them — tests are documentation of intended behaviour. If they do not, write characterisation tests that capture current behaviour, including anything that looks like a bug. You are not judging the behaviour, you are pinning it so you can detect change. Make the smallest change that works, and avoid refactoring in the same commit. Once it works and is covered, refactor separately if worthwhile. Michael Feathers' advice on legacy code applies: the hard part is usually finding a seam where you can insert a test, and that often means one small enabling refactoring — extract a method, introduce a parameter — done carefully by hand. And resist the urge to rewrite. Code that looks wrong often encodes requirements nobody remembers, and those bugs come back as production incidents.
What makes a good unit test?
It tests behaviour rather than implementation, so refactoring does not break it. A test that asserts on internal method calls fails when you restructure, which trains people to delete tests. It is fast, so the suite can run constantly. Anything touching a database or network is not a unit test, and mixing them makes the whole suite slow enough that people stop running it. It is independent — no shared state, no ordering dependency, so it can run alone or in parallel and gives the same result. It has one clear reason to fail. A test asserting fifteen things tells you little when it goes red. The name says what is being tested and what is expected, so a failure is diagnosable from the report without opening the code. Arrange-act-assert structure keeps it readable. And it fails for the right reason: a test that passes when the implementation is broken is worse than no test, which is why mutation testing is a useful check on suite quality. The judgement point is mocking. Mock at architectural boundaries; mocking your own internals couples tests to structure and produces the brittleness people blame on testing itself.
When should you use mocks versus real objects in tests?
Use real objects wherever practical, and mock at boundaries you do not own or cannot afford to invoke. Good mocking targets: external services, payment gateways, email providers, and anything slow or non-deterministic such as the clock or a random source. Bad mocking targets: your own value objects and domain entities, which are cheap to construct and whose real behaviour is what you are testing; and collaborators internal to the unit under test, since mocking them couples the test to the current decomposition. The cost of over-mocking is tests that verify the implementation calls the methods you expected, rather than that the code produces the right outcome. Those tests break on every refactoring and pass when the behaviour is wrong, which is the worst combination. Prefer a fake — a working in-memory implementation of an interface — over a mock where the collaborator has meaningful behaviour. An in-memory repository is more useful than a mock repository with stubbed returns, because it behaves consistently across tests. The design signal worth heeding: if a test needs six mocks, the class has too many dependencies, and the testing pain is telling you about the design.
How do you make time-dependent code testable?
Inject the source of time rather than calling a static. Code calling LocalDateTime.now() or System.currentTimeMillis() directly cannot be tested for anything time-dependent — you cannot test expiry, scheduling, or date-boundary behaviour without waiting or manipulating the system clock. Java provides Clock precisely for this. Inject a Clock, use LocalDateTime.now(clock), and tests can supply Clock.fixed at any instant. Testing what happens at a month boundary, or a year later, becomes trivial. The same principle applies to other ambient dependencies: randomness — inject a Random or a supplier, so tests are deterministic; UUID generation; and the current user or request context. The general rule is that any call to a static method returning a varying value is an untestable dependency in disguise. It does not appear in the constructor, so the coupling is invisible, and it cannot be substituted. This is also why time zones cause so many bugs: code using the system default zone behaves differently on a developer machine and a UTC server. Injecting the zone alongside the clock makes that explicit and testable rather than environmental.
What is the difference between a stub, a mock, a fake and a spy?
They are all test doubles, and the distinctions matter because people use "mock" for all of them and then argue past each other. A stub returns preprogrammed answers. It has no assertions — it exists to let the code under test proceed. "When asked for user 5, return this user." A mock has expectations about how it is called, and verification is part of the test. "Assert that sendEmail was called exactly once with this address." That makes it an interaction test rather than a state test. A fake is a working implementation, simplified for testing — an in-memory repository backed by a map. It has real behaviour, so it can be used across many tests without per-test setup. A spy wraps a real object and records how it was used, letting you assert on interactions while real behaviour still happens. The practical guidance: prefer fakes and stubs, use mocks sparingly for genuinely important interactions such as "the payment was actually charged", and avoid verifying every call. Over-verification is what makes test suites brittle, and it is the most common testing mistake in enterprise codebases.
How do you decide whether code is over-engineered?
Ask what each abstraction is buying you right now, and whether the cost is paid by every reader. The signals: interfaces with exactly one implementation and no plausible second one. Configuration for things nobody configures. A factory for an object that could be constructed directly. Generic type parameters used with one type. Layers that only forward calls. Extension points nobody extends. Each of those adds indirection, and indirection is paid for on every read by every person who touches the code, forever. The honest test is to ask what would break if you inlined the abstraction. If the answer is nothing except a rule someone recited, it is over-engineered. The counter-consideration is that some abstractions are cheap insurance at boundaries — wrapping a third-party library, or a repository interface — because the cost of retrofitting them later is high and the indirection is one level. The distinction is between abstraction at a boundary, where change genuinely arrives, and abstraction in the middle of your own code, where you control both sides and can refactor freely. Under-engineering is also real, but over-engineering is more common in interviews because candidates want to demonstrate pattern knowledge.
How do you handle a design decision you disagree with in review?
Separate the objective from the subjective, and be explicit about which you are raising. Objective problems — a race condition, a resource leak, an unhandled failure, a security issue — should be stated plainly as problems with the specific consequence named. "This will double-charge if the callback is retried" is actionable in a way that "this feels wrong" is not. Subjective preferences — naming, structure, whether a pattern is warranted — should be flagged as preferences, and you should be willing to lose. Blocking a review on taste is corrosive, and reviewers who do it get ignored on the things that matter. When you genuinely think a design choice will cause trouble, the useful move is to describe the future scenario rather than assert the principle. "When we add the second payment provider, this switch will need editing in four places" invites a concrete response; "this violates Open/Closed" invites an argument about principles. And propose the alternative concretely, including its cost. A criticism without an alternative is hard to act on. If you still disagree after discussion, say so, defer, and note it — being right later is worth less than being someone people want to review with.
What is the difference between designing for change and speculative generality?
Designing for change means making the code easy to modify. Speculative generality means building the modification before it is requested. The first is about structure: clear boundaries, good names, low coupling, focused classes. That makes any future change cheaper without predicting which change. The second is about anticipation: an extension point for a requirement you imagine, a configuration option nobody asked for, a plugin interface with one plugin. That predicts a specific change, and predictions are usually wrong. The cost asymmetry is what settles it. Good structure costs little and helps regardless of what changes. A speculative abstraction costs complexity permanently and helps only if you guessed right — and if you guessed wrong, it actively obstructs the change that does arrive, because you now have to remove it first. So the discipline is to invest in structure and resist features. Keep things separable so you can extract an interface when the second implementation appears, but do not create the interface until it does. The refactoring is cheap when the code is well-structured and covered by tests, which is exactly why the structural investment pays and the speculative one does not.
What are interviewers actually assessing in a low-level design round?
Whether you can turn an ambiguous problem into working, changeable code — and whether you can explain your reasoning. The specific things they watch for: do you clarify scope before coding, or start typing immediately? Do you identify the entities and their relationships, or produce a procedural script? Do you put behaviour with data, or write anemic classes plus a service that does everything? Do you leave extension points where variation is likely — pricing, scheduling, allocation — because the follow-up question is almost always "now make it support X"? Do you handle the concurrency case if one exists? Most of these problems have one, and noticing it unprompted is a strong signal. Do you use patterns where they fit, and not where they do not? Applying Abstract Factory to a two-class problem reads as pattern-matching rather than judgement. And can you defend a choice, including its downsides? "I used composition here because inheritance would couple these, though it costs a delegation layer" is a better answer than a rule recited. Working code with a clear rationale beats an elaborate design that does not run.