Low-level designCore9 min
Design anti-patterns
An anti-pattern is a common design that looks like a sensible solution and costs more than it gives, where a better solution is known. Five turn up in almost every codebase: the god object, the anemic domain model, inheritance for reuse, the singleton as a global, and the service locator.
Trip sign-ups, built from habits
A data class, a service with the rules, a global registry, and a list subclass. Each is a familiar move.
1 · The situation
Sign-ups for a school trip
A school runs trip sign-ups through a small app. A Trip has a name, a number of seats and the students signed up. TripService.sign() looks the trip up, refuses a student who's already signed or a trip that's full, and adds the rest. Trips live in Registry, which the whole program reaches through Registry.get().
Each piece is a familiar move: getters and setters on the data, a service class for the logic, one shared registry, and a SignupList that extends the standard list, adds a seat check, and gets every other list method for free. Python writes get_name() and the other accessors in snake case.
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
// Adds a seat check, and inherits the rest of the list for free.
class SignupList extends ArrayList<String> {
boolean signUp(String student, int seats) {
return size() < seats && add(student);
}
}
class Trip {
private String name;
private int seats;
private SignupList signed = new SignupList();
String getName() { return name; }
void setName(String name) { this.name = name; }
int getSeats() { return seats; }
void setSeats(int seats) { this.seats = seats; }
SignupList getSigned() { return signed; }
}
final class Registry {
private static final Registry INSTANCE = new Registry();
private final Map<String, Trip> trips = new HashMap<>();
private Registry() {}
static Registry get() {
return INSTANCE;
}
void add(Trip trip) {
trips.put(trip.getName(), trip);
}
Trip trip(String name) {
return trips.get(name);
}
}
class TripService {
String sign(String tripName, String student) {
Trip trip = Registry.get().trip(tripName);
if (trip.getSigned().contains(student)) return student + " already signed for " + tripName;
if (!trip.getSigned().signUp(student, trip.getSeats())) return tripName + " is full";
return student + " signed for " + tripName;
}
}
public class Main {
public static void main(String[] args) {
Trip museum = new Trip();
museum.setName("Museum");
museum.setSeats(2);
Registry.get().add(museum);
TripService service = new TripService();
System.out.println(service.sign("Museum", "Ana"));
System.out.println(service.sign("Museum", "Ben"));
System.out.println(service.sign("Museum", "Cy"));
System.out.println(service.sign("Museum", "Ana"));
// Any code can reach the list, and the inherited add() skips the seat check.
museum.getSigned().add("Dee");
int taken = museum.getSigned().size();
System.out.println("Museum has " + taken + " of " + museum.getSeats() + " seats");
}
}The class diagram of this version, drawn from its code: the service holds the rules, the trip holds the data, and the registry holds itself.
Ana signed for Museum Ben signed for Museum Museum is full Ana already signed for Museum Museum has 3 of 2 seats
The service's rules hold, until the last line: a museum trip with two seats has three students.
2 · The cost
Rules that anyone can walk around
The last line is the problem made visible. The seat check lives in SignupList.signUp(), called from TripService, but Trip.getSigned() hands the list to anyone, and the list also has every method it inherited: add(), remove(), clear(), none of which check seats. Any code holding a trip can change its sign-ups without passing the rule. The program does exactly that, with a plain add().
class TripService {
String sign(String tripName, String student) {
Trip trip = Registry.get().trip(tripName);
if (trip.getSigned().contains(student)) return student + " already signed for " + tripName;
if (!trip.getSigned().signUp(student, trip.getSeats())) return tripName + " is full";
return student + " signed for " + tripName;
}
}Three anti-patterns made that possible, and a fourth is waiting:
Anti-pattern | Here | What it costs |
|---|---|---|
Anemic domain model |
| The trip can't protect itself; any caller can break the seat rule |
Inheritance for reuse |
| It inherits |
Singleton as a global, used as a service locator |
| Hidden dependencies, shared state between tests, one registry for the whole process |
God object, in waiting |
| Grows with each feature: waiting lists, consent forms, payments |
The practice checks mark Trip as data with no behavior, and they recognize Registry as a Singleton. Recognizing isn't approving: the checks see the shape, and whether a global is the right choice is a design question. The inheritance and the locator they can't judge at all.
3 · The fix
Rules with their data, parts held, collaborators passed in
Each anti-pattern has a well-known better answer, and here each is small:
Trip.sign(student) checks for a duplicate and for space, then adds. The setters go: a trip gets its name and seats when it's made. The sign-up list is a private field, so the only way to add a student is through the rule.
class Trip {
private final String name;
private final int seats;
private final List<String> signed = new ArrayList<>();
Trip(String name, int seats) {
this.name = name;
this.seats = seats;
}
String name() {
return name;
}
String sign(String student) {
if (signed.contains(student)) return student + " already signed for " + name;
if (signed.size() >= seats) return name + " is full";
signed.add(student);
return student + " signed for " + name;
}
String occupancy() {
return name + " has " + signed.size() + " of " + seats + " seats";
}
}Ana signed for Museum Ben signed for Museum Museum is full Ana already signed for Museum Museum has 2 of 2 seats
The same sign-ups, and two of two seats: there's no way left to add a student without the rule.
4 · The catalog
Five anti-patterns and their answers
An anti-pattern is more than a mistake. It's a solution people reach for often, because it looks sensible and works at first, whose costs outgrow its benefits, and for which a better solution is known. That's what makes it worth naming: you'll meet it, and you'll be tempted by it.
Anti-pattern | Why it's tempting | The better answer |
|---|---|---|
God object | One obvious place to put the next feature | Split by reason to change (Single responsibility) |
Anemic domain model | Data classes are quick to write and easy to map to a database | Put the rules with the data they protect |
Inheritance for reuse | A subclass gets all the parent's methods for free | Hold an instance and expose what you mean to (Composition over inheritance) |
Singleton as a global | Any code can reach it, with no wiring | Create one instance at the root and pass it in (Dependency injection) |
Service locator | A class can fetch whatever it needs | Ask for collaborators in the constructor |
After the fix: the trip owns its rules and its list, and the desk is given the book it uses.
The five are related. A global registry makes it easy to reach any object, so logic collects in service classes; the services work on data classes, so the data stays anemic; and the service that everything uses becomes the god object. Fixing one often loosens the others.
5 · When it's fine
Context turns some anti-patterns into fair choices
Each of these has a context where it's reasonable:
- Plain data at the edges. Data transfer objects for an API or a file format are meant to carry data; calling them anemic misreads their job.
- One instance that really is process-wide. A logger or the program's configuration can reasonably be created once; still create it at the root and pass it in where you can.
Two more contexts:
- Inheritance that is an "is a". Extending a framework's base class, or a true subtype, isn't reuse by inheritance; it's inheritance.
- A locator inside a framework. Plug-in systems sometimes need to look services up by name; keep the lookup at the edge and inject into everything else.
Design | Anti-pattern when | Fine when |
|---|---|---|
Data classes with no behavior | They're the core model, with the rules elsewhere | They carry data across a boundary |
One shared instance | Code everywhere reaches for it by a static call | It's created once at the root and passed in |
Extending a library class | To borrow its methods | The subclass truly is one, and keeps every promise |
6 · Getting it right
Recognize the shape early
- Watch where new code goes. If every feature lands in the same service, that's a god object forming.
- Ask what protects the rule. If a rule lives in a different class from the data it guards, someone can walk around it.
- Ask what a subclass inherits. Everything the parent can do, the subclass now offers to every caller.
- Search for static access. Each
get()on a global is a dependency nobody declared. - Fix the cheapest one first. Making a collection private, or passing one object in, often loosens the rest.
Language | A common trap |
|---|---|
Java | Getters that return the internal mutable list |
C++ | Deriving from |
JavaScript | A module-level object that every file imports and changes: a singleton by another name |
Python | Subclassing |
7 · Look-alikes
Anti-patterns, smells and patterns
The vocabulary overlaps. What separates the terms is their scale and how sure they are.
Term | Scale | What it says |
|---|---|---|
Design pattern | A few classes | A proven solution, with known costs |
Anti-pattern | A few classes up to a whole design | A popular solution whose costs outweigh its benefits, with a known better one |
Code smell | A method or a class | A sign that something may be wrong |
Bug | Behavior | Something is wrong |
A pattern can turn into an anti-pattern by overuse. Singleton is in the classic catalog; used as a convenient global it becomes the anti-pattern this page shows. The checks report the pattern because the shape is there; deciding which one you're looking at is up to you.
8 · In practice
The garage that does everything
In the parking-lot practice problem, the contractor's draft has a ParkingLot that parks cars, prices stays, caps the fee itself and prints receipts. It's a god object at an early stage, and the checks report it: the lot also overrides what pricing returns and prints output.
Fix the parking-lot draft: give each job its home, and keep the lot to parking.
Check yourself
4 questions
Takeaways
Remember this
- An anti-pattern is a tempting, common solution whose costs outweigh its benefits, with a known better answer.
- Rules belong with the data they protect; a data class with its rules elsewhere can't defend itself.
- Inheriting to reuse code hands every caller the parent's whole interface; hold an instance instead.
- A singleton used as a global hides dependencies; create one instance at the root and pass it in.
- Context matters: plain data at boundaries and one real process-wide object can be fine.