skip to content

Design in Java

The bridge from the language-neutral design canon to Java: how the type system, standard library and runtime shape SOLID, the GoF patterns, and DRY/KISS/YAGNI in practice. Interviewers expect a concrete Java example for each principle, not a recited definition.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

5

What are the SOLID principles, and how does idiomatic Java let you express each one in code?

level: middleimportance: must knowfreq 78%

answer

  1. S-O-L-I-D: one job, extend-not-edit, substitutable, small interfaces, depend on abstractions
  2. OCP & DIP both lean on interfaces/abstract classes
  3. DIP = principle, DI/constructor injection = technique
  4. LSP is a semantic contract the compiler can't enforce
  5. ISP: many small role interfaces, not one fat one

basics

~20 s

SOLID is five design rules: each class one job, open to extend but closed to change, subtypes must be substitutable, small interfaces, depend on abstractions not concretes. In Java you use interfaces, abstract classes, and dependency injection to follow them.

solid answer

~40 s

SOLID is five object-oriented design principles. Single Responsibility: a class has one reason to change. Open/Closed: extend behaviour by adding new types, not editing existing code. Liskov Substitution: a subtype must honour its supertype's contract so callers can swap it safely. Interface Segregation: prefer many small, role-focused interfaces over one fat one. Dependency Inversion: high-level code depends on abstractions, with concretes injected. Java realizes these directly: interfaces and abstract classes give you the abstractions for OCP and DIP; `extends`/`implements` plus overriding express substitutable subtypes for LSP; splitting a `interface` into focused ones satisfies ISP; constructor injection (often via a framework like Spring) wires the concrete behind the abstraction for DIP. The payoff is code that's testable (you can substitute fakes), extensible without ripple edits, and loosely coupled.

code

java · 17 lines
java
// OCP + DIP via an interface (the abstraction)
interface PaymentMethod { void pay(long cents); }

class CardPayment implements PaymentMethod {
    public void pay(long cents) { /* charge card */ }
}
class CryptoPayment implements PaymentMethod {   // OCP: new behaviour = new class, no edits
    public void pay(long cents) { /* charge wallet */ }
}

class Checkout {
    private final PaymentMethod method;          // DIP: depend on abstraction
    Checkout(PaymentMethod method) {             // DI: injected, not new'd here
        this.method = method;
    }
    void complete(long cents) { method.pay(cents); }
}

go deeper

for a junior

Can name the five letters and give a one-line meaning for each, and knows interfaces are the main Java tool for OCP/DIP.

for a middle

Can map each principle to a concrete Java mechanism, write a small refactor that applies one, and explain DI via constructor injection.

for a senior

Discusses trade-offs (over-abstraction vs YAGNI), distinguishes DIP from DI, gives a concrete LSP violation, and applies SOLID pragmatically in real designs.

for a principal

Frames SOLID as a means to manageable change/coupling at system scale, weighs it against module boundaries, team cost, and simplicity, and knows when NOT to apply it.

## What SOLID is **SOLID** is an acronym for five object-oriented design principles, popularized by Robert C. Martin. They are guidelines for arranging classes and their dependencies so that software stays easy to change. They are *not* Java-specific — they apply to any OO language — but Java's type system gives each one a concrete shape. First, some terms: - A **class** is a blueprint for objects (data + behaviour). - An **interface** in Java is a pure contract: a named set of method signatures with no (or only default) implementation. A class `implements` it and promises to provide those methods. - An **abstract class** is a partially-implemented class that cannot itself be instantiated; subclasses `extend` it and fill in the missing (`abstract`) methods. - A **dependency** is any other type a class needs to do its work (it 'depends on' it). - **Coupling** is how tightly two pieces of code are bound; **cohesion** is how focused a single piece is. ## The five principles **S — Single Responsibility Principle (SRP).** A class should have *one reason to change* — one job, one concern. A class that both parses a file *and* formats a report *and* writes to a database has three reasons to change; split it into three. In Java this usually means smaller classes that collaborate, each holding one of the others as a field. **O — Open/Closed Principle (OCP).** Software entities should be *open for extension but closed for modification*: you add new behaviour by adding new code (a new class/implementation), not by editing tested, working code. Java expresses this with **polymorphism**: define an `interface PaymentMethod`, and add a new payment type by writing a new `class CryptoPayment implements PaymentMethod` — the dispatcher that loops over `PaymentMethod`s never changes. **L — Liskov Substitution Principle (LSP).** If `S` is a subtype of `T`, you must be able to use an `S` wherever a `T` is expected *without breaking correctness*. A subclass must honour the superclass's contract: not strengthen preconditions, not weaken postconditions, not throw new checked exceptions, not return `null` where the parent promised non-null. The classic violation is a `Square extends Rectangle` whose `setWidth` secretly also changes the height — code written against `Rectangle` then misbehaves. In Java, `@Override` and covariant return types help, but LSP is a *semantic* contract the compiler can't fully enforce. **I — Interface Segregation Principle (ISP).** Clients should not be forced to depend on methods they don't use. Prefer several small, **role interfaces** over one large 'fat' interface. If one giant `interface Machine { print(); scan(); fax(); }` forces a simple printer to implement `fax()` with a stub, split it into `Printer`, `Scanner`, `Fax`. Java's support for implementing *multiple* interfaces makes this cheap. **D — Dependency Inversion Principle (DIP).** High-level modules (policy) should not depend on low-level modules (details); both should depend on **abstractions**. And abstractions should not depend on details — details depend on abstractions. Concretely: an `OrderService` should depend on an `interface OrderRepository`, not on a concrete `JdbcOrderRepository`. The concrete is *injected* from outside. **Dependency Injection (DI)** — usually constructor injection — is the technique; in enterprise Java a container like Spring wires it. Note DIP (the principle) and DI (the technique) are related but not the same. ## How Java's features map on | Principle | Java mechanism | |---|---| | SRP | small focused classes, composition | | OCP | interfaces/abstract classes + polymorphism; new `implements` | | LSP | `extends`/`@Override`, covariant returns, honouring contracts | | ISP | many small interfaces, multiple `implements` | | DIP | program to interfaces, constructor injection / Spring | ## Why it matters Following SOLID makes code **testable** (you can inject a fake `OrderRepository` in a unit test), **extensible** (new behaviour = new class, no edits to working code → fewer regressions), and **loosely coupled** (modules can evolve independently). The cost is more types and indirection — applied dogmatically it produces needless abstraction, so balance it with YAGNI ('You Aren't Gonna Need It').

  • Is Dependency Inversion the same thing as Dependency Injection?
    No. Dependency Inversion is the principle (depend on abstractions, inject the concrete from outside). Dependency Injection is the common technique that implements it — supplying a class's collaborators via its constructor (or setter/field) rather than having the class construct them itself. You can apply DIP without a DI framework, and a DI framework just automates the wiring.
  • Give a Java example that violates LSP.
    A `Square extends Rectangle` where `setWidth(w)` also sets the height (to keep it square). Code written against `Rectangle` that sets width to 5 and height to 4 then asserts area == 20 breaks, because the square forced height to 5. The square is not substitutable for a rectangle; composition or a shared `Shape` interface is the fix.

saying these in an interview costs you the question

  • Saying OCP means 'never change a class' — it means don't change it to add new variants; bug fixes are fine.
  • Conflating Dependency Inversion (principle) with Dependency Injection (technique).
  • Claiming the compiler enforces LSP — it only checks signatures, not the semantic contract.
  • Treating SOLID as mandatory everywhere, producing over-abstracted code that violates YAGNI/KISS.
  • Thinking ISP requires interfaces with exactly one method — it's about not forcing unused dependencies, not method count.

context

open as a page

What do DRY, KISS, YAGNI, and separation of concerns mean, and how do you apply them when writing Java?

level: juniorimportance: should knowfreq 62%

basics

~10 s

DRY: don't repeat the same logic — extract it into one method. KISS: keep it simple. YAGNI: don't build features you don't need yet. Separation of concerns: each class/method does one kind of job.

open as a page

How do classic GoF design patterns show up in the Java standard library? Give concrete JDK examples.

level: middleimportance: should knowfreq 70%

basics

~20 s

The JDK is full of GoF patterns: BufferedInputStream wrapping a stream is Decorator, Comparator passed to sort is Strategy, Calendar.getInstance() is a Factory, Runnable submitted to an executor is Command, and Iterator/Iterable is the Iterator pattern.

open as a page

How have functional interfaces and lambdas reshaped how classic patterns like Strategy are written in modern Java?

level: seniorimportance: should knowfreq 55%

basics

~20 s

A functional interface has one abstract method, so you can pass a lambda where it's expected. That turns patterns like Strategy into a one-line lambda instead of a whole class — e.g. passing a Comparator to sort, or a Runnable to a thread.

open as a page

How do you decide when applying SOLID and GoF patterns adds value versus when it becomes over-engineering in a Java codebase?

level: principalimportance: should knowfreq 40%

basics

~20 s

Patterns and SOLID earn their cost only when there's real, recurring change. If a problem is simple and stable, a plain class or method beats an interface plus a pattern. Add abstraction when a second real case appears, not on speculation.

open as a page