Guide · System design
Design Patterns Every Engineer Should Know
29 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- Design patterns are named, reusable designs for problems that keep coming back in object-oriented code, where a program is built from objects that hold data and behaviour. They are ideas you adapt, not code you copy.
- The classic list is the 23 patterns in a 1994 book by four authors, nicknamed the Gang of Four, in three families. Creational patterns shape how objects get made, structural ones how parts fit together, and behavioural ones how objects share work.
- This guide teaches nine that fit everyday backend code, each on one online store: Factory Method, Builder, Singleton, Adapter, Decorator, Facade, Strategy, Observer and State.
- Most patterns cost extra classes and indirection, meaning more hops between files before you reach the code that runs. Add one when a real change hurts, and first check whether a lambda, a short function written inline, already does the job.
- Beginners can stop after the section on when not to use a pattern. After that, engineers get what we found running every sample on OpenJDK 21, a free build of Java. Java's old Observable class, its built-in event announcer, calls listeners (the code that reacts) in reverse order, and one failing listener silences the rest. A rupees-to-paise adapter using double, Java's floating-point number type, lost a paisa (a hundredth of a rupee) on 4,586 of 99,999 prices.
What goes wrong when every new rule lands in one checkout class?
Each new rule becomes one more if-else branch inside the same big class. Every change then touches code that other features depend on, so a small edit breaks something unrelated. Design patterns are the named, well-known ways out of that trap.
Picture a common setup. An online saree and fabric store in Surat takes about 1,200 orders a day. Almost all of its order logic sits in one Java class called CheckoutService, which started at 300 lines and is now 2,400. The setup is made up, but the problem is real.
Before the Diwali sale, a junior developer adds a new courier for Gujarat deliveries. The new branch reuses a variable that the cash-on-delivery code also reads. Three kinds of trouble now show up together.
- Cash-on-delivery orders get the wrong shipping charge, and about 300 orders are stuck for four hours before anyone notices.
- Adding a second payment gateway, the outside service that actually moves the money, has already taken three weeks, because gateway calls are spread across nine methods.
- Every new message to the buyer, such as a WhatsApp update or a loyalty points note, means editing the checkout method again.
The bug is not really the junior developer's fault. The code keeps solving three problems by hand: choosing between rules, fitting outside services, and telling other parts that something happened. Each problem has a well-known name and a well-known answer.
This guide fixes each one with a pattern, and also shows when to leave code alone. Every Java sample is a complete file that runs with one command. We ran each one on OpenJDK 21.0.12, a free build of Java, and all but the last also on 17.0.20.
What are design patterns?
A design pattern is a named, proven way to arrange classes and objects to solve a problem that keeps coming back. It describes the shape of a solution, which you then fit to your own code.
The idea became famous through the book Design Patterns: Elements of Reusable Object-Oriented Software, first released in October 1994 with a 1995 copyright date. Its four authors, Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, are often called the Gang of Four. The book lists 23 patterns.
Think of a weaver's pattern book. It holds named motifs, from a small repeated flower to an all-over net design. A weaver does not copy a whole saree from the book. She picks a motif, then sizes and colours it for the cloth on her loom.
- The motif's name is the pattern's name. Two engineers can say "put an adapter in front of the gateway" and both picture the same shape.
- Sizing and colouring the motif is fitting the pattern to your classes. Two correct uses of the same pattern can look quite different in code.
- Choosing a motif for this cloth is choosing a pattern for this problem, and some cloth needs no motif at all.
Where the picture breaks: a motif only changes how cloth looks, but a pattern changes how code behaves as it grows. Each pattern in the book also has a Consequences section that lists what you give up.
Which words do you need before learning design patterns?
You need five words: class, object, interface, inheritance and composition. The table gives each one a plain meaning and an example from the store.
| Word | Plain meaning | Store example |
|---|---|---|
| Class | A blueprint in code: what data something holds and what it can do | A SareeOrder class holds the product code, the blouse size and the price |
| Object | One real thing made from a class while the program runs | The order for a red Banarasi saree placed at 11 am |
| Interface | A promise that a class offers certain methods, without saying how | PaymentGateway promises a charge method, and each gateway keeps it in its own way |
| Inheritance | One class extends another and reuses its code | ExpressCourier extends Courier and often depends on the parent's insides |
| Composition | An object holds other objects and hands work to them | Checkout holds a ShippingRule object and asks it for the cost |
The Gang of Four book gives two rules that run under almost every pattern. The first is "Program to an interface, not an implementation." The second is to favour object composition over class inheritance.
The reason is visibility and change. A subclass often depends on its parent's insides, so a change to the parent can break it. A held object is used only through its interface, so you can swap it while the program runs.
What are the three types of design patterns?
The three types are creational, structural and behavioural. Creational patterns deal with how objects get made, structural patterns with how parts fit together, and behavioural patterns with how objects share the work.
In the book, the creational family has 5 patterns, the structural family 7 and the behavioural family 11. You do not need all 23 to start. Begin from the pain, then find the family.
Start at the question box at the top of the diagram. Each arrow names a kind of pain and leads to the family that treats it.
Finding the right family of patterns
Drag to exploreName the pain first. The family follows from it, and the pattern from the family.
Diagram connections:
- What keeps hurting? to Creational patterns (Making objects is messy)
- What keeps hurting? to Structural patterns (Parts do not fit)
- What keeps hurting? to Behavioural patterns (Rules keep changing)
- Creational patterns to Builder, Factory Method
- Structural patterns to Adapter, Decorator, Facade
- Behavioural patterns to Strategy, Observer, State
- What keeps hurting?: Look at the last three painful changes
- Creational patterns: How objects get made
- Structural patterns: How parts fit together
- Behavioural patterns: How objects share the work
- Builder, Factory Method: Saree orders with many options, courier labels
- Adapter, Decorator, Facade: Two payment gateways with different APIs
- Strategy, Observer, State: Shipping rules, order events, order status
The table lists the nine patterns this guide covers. Read the last column as carefully as the others, because it says when the pattern costs more than it saves.
| Pattern | Family | Problem it solves | Store example | Skip it when |
|---|---|---|---|---|
| Factory Method | Creational | Callers should not pick the concrete class | Courier labels, as a simple factory or the book's form | There is only one kind of object |
| Builder | Creational | Objects with many optional parts | A saree order with fall, pico and blouse size | The object has two or three fields |
| Singleton | Creational | Exactly one shared instance | Rarely needed; config is passed in instead | Almost always; pass the object in |
| Adapter | Structural | A class has the wrong interface | Wrap each payment gateway's API | You own both sides and can change them |
| Decorator | Structural | Add behaviour without changing a class | Log every gateway call | Only one fixed extra is ever needed |
| Facade | Structural | Too many parts to call in order | One placeOrder method for the app | Callers really need the fine control |
| Strategy | Behavioural | One job, several ways to do it | Shipping cost rules per courier | There is one rule that never changes |
| Observer | Behavioural | Many parts react to one event | Email, SMS and warehouse after an order | Only one listener exists, or listeners must run in a set order |
| State | Behavioural | Behaviour depends on the current status | Placed, packed, shipped, delivered | There are two states and one flag |
Which creational design patterns should you learn first?
Learn Factory Method and Builder first, and learn Singleton mainly so you can recognise its problems. Together they cover the object-creation trouble in the store's checkout.
Factory Method: the book's form and the simple factory
A factory puts the choice of which class to create in one place, so the rest of the code never names the concrete class. Callers ask for what they need, not for an exact type.
The book's version is stricter. Its intent is to "let subclasses decide which class to instantiate": a base class calls a creation method, and each subclass overrides it. The everyday form that follows, one method with a switch inside, is often called a simple factory.
In the store, each courier needs a different shipping label format. CheckoutService used to switch on the courier name in four places. The team first moves that choice into one simple factory method, labelFor(courier), which returns a ShippingLabel. Adding a courier now means one new class and one new line in that method.
The book's form drops the switch. An abstract Courier class, which cannot be created on its own, declares createLabel(), a method with no body. Its ship method calls createLabel without naming any label class. Each courier subclass, such as ExpressCourier, overrides createLabel to return its own label, so a new courier is one new subclass.
Factory Method's bigger cousin, Abstract Factory, creates whole families of related objects that must match, such as buttons and menus for one visual theme. A store backend rarely needs it.
Builder: make objects with many optional parts
Builder lets you create an object step by step, setting only the parts you need, and then call build at the end. It replaces long constructors where nobody remembers what the sixth true or false means.
A saree order fits well. Buyers can add fall and pico, the finishing work on a saree's edges, and can pick a blouse size or leave the blouse piece unstitched. Save the file as BuilderDemo.java and run java BuilderDemo.java.
public class BuilderDemo {
public static void main(String[] args) {
SareeOrder order = new SareeOrder.Builder("BNR-1042")
.withFallPico()
.blouseSize("38")
.build();
System.out.println(order);
System.out.println(new SareeOrder.Builder("GRG-0007").build());
}
}
class SareeOrder {
private final String sku;
private final boolean fallPico;
private final String blouseSize;
private SareeOrder(Builder b) {
sku = b.sku; fallPico = b.fallPico; blouseSize = b.blouseSize;
}
@Override public String toString() {
return sku + " fallPico=" + fallPico + " blouse=" + blouseSize;
}
static class Builder {
private final String sku;
private boolean fallPico = false;
private String blouseSize = "unstitched";
Builder(String sku) { this.sku = sku; }
Builder withFallPico() { fallPico = true; return this; }
Builder blouseSize(String size) { blouseSize = size; return this; }
SareeOrder build() { return new SareeOrder(this); }
}
}You should see two lines: BNR-1042 fallPico=true blouse=38, then GRG-0007 fallPico=false blouse=unstitched. The second order shows the defaults the Builder fills in.
Three things matter here. The SareeOrder constructor is private, so the only way in is through the Builder. Each Builder method returns this, which lets calls chain. The fields are final and there are no setters, so nothing can change an order after build.
Singleton: one instance, and many warnings
Singleton makes sure a class has exactly one instance and gives the whole program one global way to reach it. That sounds tidy, but it usually hides a global variable behind a class name.
The trouble shows up in tests. Say PriceConfig is a Singleton and one test sets a 20 percent festival discount. The next test in the same JVM, the Java virtual machine that runs your code, still sees 20. We ran that two-step check, and the second step printed 20.
Even one of the four authors has turned against it. In a 2009 InformIT interview, Erich Gamma said he was in favour of dropping Singleton. He called its use "almost always a design smell."
Which structural design patterns does a backend like this need?
This checkout needs three. Adapter makes a mismatched class fit, Decorator adds behaviour around an object, and Facade gives a messy group of classes one simple front door.
Adapter: make a mismatched API fit
Adapter wraps a class whose methods do not match what your code expects, and translates between the two. Your code talks to one interface, and the adapter talks to the outside API.
The store's card gateway wants the amount in paise (100 paise make one rupee) and returns numeric error codes. The new UPI gateway wants rupees and returns text statuses. The team writes one PaymentGateway interface and one adapter per gateway.
Two payment gateways behind one interface
Drag to exploreNotice that Checkout only ever sees the interface. Each adapter hides its gateway's quirks, so a third gateway means one new adapter.
Diagram connections:
- Checkout service to PaymentGateway interface
- PaymentGateway interface to Card gateway adapter (Card payment)
- PaymentGateway interface to UPI gateway adapter (UPI payment)
- Card gateway adapter to Card gateway
- UPI gateway adapter to UPI gateway
- Checkout service: Calls charge() and nothing else
- PaymentGateway interface: One promise: charge(order, amount)
- Card gateway adapter: Converts rupees to whole paise, maps error codes
- UPI gateway adapter: Builds the UPI request, reads text status
- Card gateway: Outside service with an older API
- UPI gateway: Outside service with a newer API
This is why the second gateway took three weeks. Gateway details were spread across nine methods. With adapters, they live in two small classes, and a gateway bug stays inside its own adapter.
Decorator: add behaviour around an object
Decorator wraps an object in another object that has the same interface and adds something before or after each call. Callers cannot tell the difference, because both offer the same methods.
The team wants every payment call logged with the time it took. LoggingGateway implements PaymentGateway and holds another PaymentGateway inside. It notes the start time, calls the inner gateway and logs the result.
Java's own input and output classes use the same shape. The BufferedReader docs suggest wrapping one around another Reader, such as a FileReader, so that reads are buffered.
Facade: one simple front door
Facade gives callers one simple method in front of several parts that must be called in the right order. The parts still exist; the facade just hides the order of calls.
Placing an order touches stock, payment, shipping and the invoice. The mobile app should not need to know that order. So the team adds OrderFacade with one method, placeOrder, which calls the four parts in sequence and returns one result.
Which behavioural design patterns fix rules that keep changing?
Three of them do: Strategy, Observer and State. Strategy swaps one rule for another, Observer lets many parts react to one event, and State changes behaviour as an order's status changes.
Strategy: swap the rule, keep the caller
Strategy puts each version of a rule in its own object behind one interface, and the caller holds whichever one it was given. Changing the rule means passing a different object, not editing the caller.
This is the fix for the Diwali bug. Each shipping rule becomes its own class, so a new courier cannot touch the cash-on-delivery code. Order here is a record, a short Java class that only holds data.
public class StrategyDemo {
public static void main(String[] args) {
Order order = new Order(2499, 2);
System.out.println(new Checkout(new CourierRule()).total(order));
System.out.println(new Checkout(o -> 0).total(order));
}
}
record Order(int itemsTotal, int weightKg) {}
interface ShippingRule {
int costInRupees(Order order);
}
class CourierRule implements ShippingRule {
public int costInRupees(Order order) {
return 60 + 25 * order.weightKg();
}
}
class Checkout {
private final ShippingRule shipping;
Checkout(ShippingRule shipping) { this.shipping = shipping; }
int total(Order order) { return order.itemsTotal() + shipping.costInRupees(order); }
}Run java StrategyDemo.java and you should see 2609, then 2499. The courier rule adds 60 + 25 x 2 = 110 rupees to a 2,499 rupee order, and the shop pickup rule adds nothing.
The key line is the Checkout constructor. Checkout receives a ShippingRule and never asks which one it is. The second line of main passes a lambda that always returns 0, so shop pickup needs no class at all.
Oracle's Java tutorial calls an interface with one abstract method, a method with no body, a functional interface. It says lambda expressions express instances of such single-method classes more compactly. So a small Strategy can shrink to a lambda.
Observer: many parts react to one event
Observer lets one object announce that something happened while any number of listeners react, without the announcer knowing who they are. New reactions are added by registering a listener, not by editing the announcer.
After an order, the store sends an email receipt and a tracking SMS. It also asks the warehouse to pack the parcel and adds loyalty points. Follow the arrows in the diagram.
One order event, many listeners
Drag to exploreThe order service publishes once and never learns who listens. A fifth listener, such as a WhatsApp update, plugs in without touching it.
Diagram connections:
- Order service to OrderPlaced event
- OrderPlaced event to Email listener
- OrderPlaced event to SMS listener
- OrderPlaced event to Warehouse listener
- OrderPlaced event to Loyalty points listener
- Order service: Publishes OrderPlaced once per order
- OrderPlaced event: Keeps the list of registered listeners
- Email listener: Sends the order receipt
- SMS listener: Sends the tracking text
- Warehouse listener: Asks the warehouse team to pack the parcel
- Loyalty points listener: Adds points to the buyer's account
Observer has a known weak spot: order and failure. Java's own java.util.Observable class has been deprecated since Java 9, and its docs name unspecified notification order as one reason. In the Java 21 docs it is deprecated but not marked for removal.
So decide two things up front. First, whether any listener must run before another. Second, what happens if the SMS listener throws an error halfway. The engineer section runs Java's old class to show why both matter.
State: let the status decide the behaviour
State gives each status of an object its own class, and each class decides which actions are allowed. It replaces the same switch on status repeated in many methods.
An order here moves from placed to packed to shipped to delivered. Cancelling is free while placed, costs a fee once packed, and is blocked once shipped. With State, each status is one small class that holds its own rules, instead of the same switch copied into five methods.
How do design patterns relate to the SOLID principles?
SOLID principles are goals for well-shaped classes, and design patterns are common ways to reach those goals. A principle tells you what good looks like, and a pattern shows one known route there.
SOLID is five rules of thumb for object-oriented design, named by their first letters. The SOLID principles guide covers each one in depth. Here is how the patterns above line up with three of them.
- Open for extension, closed for change: Strategy and Observer let the store add a courier or a listener without editing Checkout.
- Single responsibility: Adapter keeps gateway quirks in one class, so CheckoutService no longer has a reason to change when a gateway does.
- Depend on abstractions: every pattern here makes callers hold an interface, like ShippingRule or PaymentGateway, rather than a concrete class.
The link runs one way only. Following SOLID does not mean adding patterns everywhere. Often the simplest code that respects the principles has no named pattern at all.
When should you not use a design pattern?
Skip a design pattern when its problem has not happened yet, or the code is small enough to read in one go. Also skip it when a plain language feature already does the job.
Most patterns add classes and indirection, meaning you jump through extra layers to find the code that actually runs. That is worth it when changes keep landing in one spot. It is pure cost when nothing changes.
Back to the weaver: a heavy all-over design suits a wedding silk. On a light cotton saree for daily wear, it only adds weight and cost.
Extreme Programming, a way of working in small and frequent steps, has a name for this: YAGNI, short for you aren't gonna need it. Martin Fowler lists four costs of building something before you need it. You pay to build it and you delay other work. You carry its complexity in every later change, and you pay to repair it if the real need turns out different.
Fowler also draws a line that matters here. YAGNI covers features built for a guessed future, not work that makes code easier to change. Refactoring into a pattern once a change really hurts is the second kind.
Language features also shrink patterns. In a talk on 5 May 1996, Peter Norvig argued that 16 of the 23 patterns are invisible or simpler in the languages Lisp and Dylan. His slides scope that to at least some uses of each pattern. His Strategy slide says the strategy is a variable whose value is a function, which is what a Java lambda gives you.
The team uses a simple check before adding any pattern. Start at the change request in the diagram and answer each question honestly.
Should this code get a pattern?
Drag to exploreMost changes end at plain code or a language feature. Only repeated pain earns a named pattern.
Diagram connections:
- New change request to Has this part changed twice?
- Has this part changed twice? to Keep the plain code (No)
- Has this part changed twice? to Can a lambda or enum do it? (Yes)
- Can a lambda or enum do it? to Use the language feature (Yes)
- Can a lambda or enum do it? to Use the named pattern (No)
- New change request: Example: add a second courier
- Has this part changed twice?: Count real changes, not guesses
- Keep the plain code: Small, direct and easy to read today
- Can a lambda or enum do it?: Java may already have the tool
- Use the language feature: A lambda, an enum or a record
- Use the named pattern: Put its name in the class and the review
These signs say a pattern is probably overkill:
- An interface has exactly one class behind it, and no second one is planned this quarter.
- A Factory only ever returns one type.
- A Strategy class has one short method that a lambda could replace.
- A teammate needs to open five files to follow one simple request.
- The pattern was added because it looked good in a review, not because a change hurt.
For the checkout, the team plans the clean-up in small steps, each one shipped on its own.
Write tests around today's behaviour
Before moving code, add tests for the shipping charge on 20 real order shapes, including cash on delivery. They prove nothing changed.
Pull out shipping first
Shipping caused the Diwali bug, so it goes first. Each courier rule becomes a ShippingRule class, and the tests must still pass.
Wrap the gateways
Move each gateway's calls into its own adapter behind PaymentGateway. Delete the gateway code from the nine old methods.
Publish the order event
Replace the direct email, SMS and warehouse calls with listeners on OrderPlaced. Decide what happens when one listener fails.
Stop there
Leave the rest of CheckoutService alone until it hurts. Two months later (made up), the class is under 900 lines and courier changes take a day.
Why did Java deprecate its built-in Observer and Observable?
The deprecation note in its Java docs says the event model is quite limited and the order of notifications is unspecified. It adds that state changes do not match notifications one for one. Running the class shows what that means in practice.
The class docs add a promise the code does not keep. They say the default implementation notifies observers "in the order in which they registered interest". The demo registers email, then an SMS listener that fails, then the warehouse.
import java.util.Observable;
public class ObservableDemo {
static class OrderEvents extends Observable {
void orderPlaced(String id, boolean markChanged) {
if (markChanged) setChanged();
notifyObservers(id);
}
}
public static void main(String[] args) {
OrderEvents events = new OrderEvents();
events.addObserver((o, id) -> System.out.println("1 email " + id));
events.addObserver((o, id) -> { throw new IllegalStateException("SMS down"); });
events.addObserver((o, id) -> System.out.println("3 warehouse " + id));
events.orderPlaced("A-1", false);
System.out.println("-- no setChanged: nothing ran");
try {
events.orderPlaced("A-2", true);
} catch (IllegalStateException e) {
System.out.println("-- stopped by: " + e.getMessage());
}
}
}Pause and predict which lines print before you run java ObservableDemo.java. Java also prints deprecation warnings (Java 21) or notes (Java 17) first; they are expected. Here is what we got on OpenJDK 21.0.12 and 17.0.20.
-- no setChanged: nothing ran
3 warehouse A-2
-- stopped by: SMS downWhat just happened? Each of the three surprises matches the JDK 21 source of Observable.notifyObservers.
- Nothing ran for order A-1, because notifyObservers returns at once unless setChanged was called first. Forgetting that one call fails silently.
- The warehouse, registered last, ran first. The loop counts down from the end of the list, so listeners run in reverse order of registration. With five listeners we saw 5 4 3 2 1.
- The SMS listener's exception escaped the loop, so the email listener, registered first, never ran. The changed flag was already cleared, so calling notifyObservers again without setChanged sends nothing.
The fix is a small event class you own. It keeps listeners in a list, calls them in the order they subscribed, and catches each failure so one broken listener cannot silence the others.
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
public class OrderEventsDemo {
public static void main(String[] args) {
OrderEvents events = new OrderEvents();
events.subscribe(id -> System.out.println("1 email " + id));
events.subscribe(id -> { throw new IllegalStateException("SMS down"); });
events.subscribe(id -> System.out.println("3 warehouse " + id));
events.publish("A-2");
}
}
class OrderEvents {
private final List<Consumer<String>> listeners = new CopyOnWriteArrayList<>();
void subscribe(Consumer<String> listener) { listeners.add(listener); }
void publish(String orderId) {
for (Consumer<String> listener : listeners) {
try {
listener.accept(orderId);
} catch (RuntimeException e) {
System.out.println("listener failed: " + e.getMessage());
}
}
}
}Expected output: 1 email A-2, then listener failed: SMS down, then 3 warehouse A-2. The list is a CopyOnWriteArrayList, which copies itself on every change. Its loop never throws ConcurrentModificationException, the error Java raises when a list changes mid-loop.
We checked that too. A listener that subscribed another listener during publish crashed a plain ArrayList loop with that error. The copy-on-write list finished, and the new listener waited for the next event.
This is still code inside one program, and a slow listener still delays checkout. Once listeners live in other services, the same idea moves to publish and subscribe over a message queue, a store that holds events until listeners collect them.
Why should a payment adapter not convert rupees to paise with a double?
Because a double, Java's binary floating-point number, cannot hold most decimal prices exactly. Casting the result to a whole number then cuts off the fraction, so whenever the product lands just under the right number, a paisa disappears.
Go back to the card adapter. The app passes 19.99 rupees, and the gateway wants a whole number of paise.
import java.math.BigDecimal;
public class PaiseDemo {
public static void main(String[] args) {
double rupees = 19.99;
System.out.println(rupees * 100);
System.out.println((long) (rupees * 100));
System.out.println(new BigDecimal("19.99").movePointRight(2).longValueExact());
}
}Run java PaiseDemo.java and you should see 1998.9999999999998, then 1998, then 1999. Multiplying gives a number just under 1999, and the cast to long drops everything after the point.
We tested every price from 0.01 to 999.99 rupees, 99,999 in all, parsed from text as an app would. The cast lost a paisa on 4,586 of them, starting at 0.29.
Math.round(rupees * 100) gave the right answer for all 99,999 in that test, but it hides the real issue. BigDecimal, Java's exact decimal type, built from the text 19.99 is exact. Its longValueExact throws an ArithmeticException for 19.999 instead of quietly rounding.
Why is an enum Singleton harder to break than a private constructor?
Because Java blocks the usual ways to make a second instance of an enum: reflection, cloning and serialisation. A private constructor only stops honest callers, and reflection, Java's way to inspect and call code while it runs, can open it.
import java.lang.reflect.Constructor;
public class SingletonDemo {
public static void main(String[] args) throws Exception {
Constructor<ClassicConfig> c = ClassicConfig.class.getDeclaredConstructor();
c.setAccessible(true);
System.out.println(c.newInstance() == ClassicConfig.INSTANCE);
Constructor<EnumConfig> e = EnumConfig.class.getDeclaredConstructor(String.class, int.class);
e.setAccessible(true);
try {
e.newInstance("SECOND", 1);
} catch (IllegalArgumentException ex) {
System.out.println(ex.getMessage());
}
}
}
class ClassicConfig {
static final ClassicConfig INSTANCE = new ClassicConfig();
private ClassicConfig() {}
}
enum EnumConfig { INSTANCE }Run java SingletonDemo.java and you should see false, then Cannot reflectively create enum objects. The first line shows reflection built a second ClassicConfig. The second is the message of the IllegalArgumentException that Constructor.newInstance throws for enum classes. The Java 21 docs promise that exception; the message text comes from Java's own code.
Serialisation, turning an object into bytes and back, is the other leak. In our test, a Serializable classic Singleton came back as a new object unless it had a readResolve method, a hook that hands back the existing instance. An enum came back as the same constant, because Java serialises enum constants by name only.
None of this fixes the testing problem from earlier. An enum Singleton is still global state, so creating one object and passing it in remains the better default.
Can a sealed interface and switch replace the State pattern in Java 21?
Sometimes. A sealed interface lists every class allowed to implement it, and since Java 21 a switch over it must cover each one. The compiler then catches a forgotten status for you.
Sealed interfaces became a standard feature in Java 17, after previews (trial versions, off by default) in Java 15 and 16. Pattern matching for switch, where each case matches a type, became final in Java 21. Here are the order rules as one switch.
public class OrderStateDemo {
public static void main(String[] args) {
System.out.println(cancelFee(new Placed()));
System.out.println(cancelFee(new Packed()));
System.out.println(cancelFee(new Shipped()));
}
static String cancelFee(OrderState state) {
return switch (state) {
case Placed p -> "free";
case Packed p -> "fee 49 rupees";
case Shipped s -> "blocked";
case Delivered d -> "blocked";
};
}
}
sealed interface OrderState permits Placed, Packed, Shipped, Delivered {}
record Placed() implements OrderState {}
record Packed() implements OrderState {}
record Shipped() implements OrderState {}
record Delivered() implements OrderState {}Run java OrderStateDemo.java and you should see free, then fee 49 rupees, then blocked. On Java 17.0.20 the same file fails to compile, because this switch was still a preview feature there.
Now check the safety net. Add record Returned() implements OrderState {} and put Returned in the permits list, the list of allowed classes. Compiling fails with "the switch expression does not cover all possible input values" until you add a case for it.
Add a default branch and the error disappears, so leave default out if you want this check. The two designs fail in opposite places. With State classes a new status is one new class, but a new action means editing every class.
With a sealed switch a new action is one new method, and the compiler flags every switch without a default that a new status breaks. Pick the side that matches what changes more often in your code.
How do you explain design patterns in a system design interview?
Patterns come up mostly in low-level design rounds, where you design the classes inside one program, such as a parking lot. Explain each pattern by the change it makes easy, show it in two or three classes, and say what it costs.
Find what varies
Read the problem and name the part most likely to change, like the pricing rule in a parking lot or the payment method in a booking system.
Name the pain
Say what goes wrong without a pattern: a switch that grows with every new type, or one class that knows every outside API.
Pick the pattern and draw it
Draw the interface, two concrete classes and the caller. Two boxes behind an interface say more than a paragraph of theory.
Show the change
Walk through adding a new type, and point out which files you touched and which you did not.
Name the cost
Say what the pattern adds: more classes, more indirection, and where a lambda, an enum or a sealed switch would be enough.
Questions people ask
- How many design patterns are there?
- The Gang of Four book, first released in 1994, lists 23 design patterns: 5 creational, 7 structural and 11 behavioural. Many more have been named since, for areas like enterprise apps, concurrency and distributed systems. The circuit breaker, popularised by Michael Nygard for failing remote services, is one well-known later example.
- Which design patterns should a beginner learn first?
- Start with Strategy, Observer, Factory Method, Builder, Adapter, Decorator and Facade. Each one solves a problem you will meet in ordinary backend code. Learn Singleton too, mostly so you can spot its problems with testing and hidden global state. Leave rarer ones like Flyweight and Interpreter for later.
- Is Singleton an anti-pattern?
- Not always, but it is overused. An anti-pattern is a common solution that tends to cause more trouble than it fixes. Singleton hides a global variable, which makes tests share state and hides what a class depends on. Erich Gamma, one of the book's authors, said in 2009 he favoured dropping it. Creating one instance at startup and passing it in usually works better.
- Are design patterns still relevant today?
- Yes, as shared vocabulary and as ways to contain change, but some have become lighter. Lambdas, records (final since Java 16), enums and sealed interfaces make several patterns shorter in modern Java. The underlying ideas, like programming to an interface and favouring composition, still apply to almost every codebase.
- What is the difference between design patterns and SOLID principles?
- SOLID principles describe what good class design looks like, and design patterns are proven ways to get there. For example, the open for extension rule says you should add behaviour without editing old code. Strategy and Observer are two patterns that make that possible.
- What is the difference between Strategy and State?
- Both put behaviour behind an interface, but they differ in who picks the object. In Strategy, the caller chooses a rule from outside, and strategies do not know about each other. In State, the state classes may know each other and switch the object to the next state, like an order moving from packed to shipped.
- Do I need design patterns for a low-level design interview?
- You need a few of them, used with judgement. Low-level design rounds ask you to model classes for a problem like a parking lot or an elevator. Strategy fits changing pricing rules, State fits an elevator's modes, and Observer fits notifications. Explaining why you chose one, and its cost, matters more than naming many.
Sources and further reading
- Design Patterns: Elements of Reusable Object-Oriented Software (Wikipedia): release date, authors, the 23 patterns by family, the two principles
- Software design pattern (Wikipedia): the Gang of Four documentation format, including Consequences
- Factory method pattern (Wikipedia): the Gang of Four intent
- Design Patterns 15 Years Later: An Interview with Erich Gamma, Richard Helm, and Ralph Johnson (InformIT, 22 October 2009)
- Design Patterns in Dynamic Programming, slides (Peter Norvig, Object World, 5 May 1996): slides 9 and 10 (16 of 23) and slide 16 (Strategy)
- Java SE 21 API: java.util.Observable (deprecated since 9; notification order)
- Java SE 21 API: CopyOnWriteArrayList (snapshot iterator)
- The Java Tutorials: Lambda Expressions (functional interfaces)
- Java SE 21 API: BufferedReader (wrapping another Reader)
- Java SE 21 API: Iterable (target of the for-each loop)
- Java SE 21 API: Constructor.newInstance (refuses enum classes)
- Java SE 21 API: Enum (clone never returns, special serialisation)
- Java Object Serialization Specification (Java SE 21), section 1.12: Serialization of Enum Constants
- JEP 409: Sealed Classes (delivered in JDK 17)
- JEP 441: Pattern Matching for switch (delivered in JDK 21; exhaustiveness)
- JEP 395: Records (final in JDK 16)
- JEP 330: Launch Single-File Source-Code Programs (JDK 11)
- Yagni (Martin Fowler, 26 May 2015)
- CircuitBreaker (Martin Fowler, 6 March 2014)
- State pattern (Refactoring.Guru): how State differs from Strategy