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.

Watch it evolve1 / 10
The problem

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.

80
Start
Stage 1
Final

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.

The first versionJava
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");
    }
}
1*SignupList~ signUp(String, int): booleanTrip− name: String− seats: int− signed: SignupList~ getName(): String~ setName(String)~ getSeats(): int~ setSeats(int)~ getSigned(): SignupListRegistry− INSTANCE: Registry− trips: Map<String, Trip>~ get(): Registry~ add(Trip)~ trip(String): TripTripService~ sign(String, String): String

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().

The rules live outside the data they protectJava
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

Trip is data with getters and setters; its rules live elsewhere, in TripService and the list

The trip can't protect itself; any caller can break the seat rule

Inheritance for reuse

SignupList extends the standard list and adds signUp()

It inherits add(), remove() and clear() too, every caller gets them, and none of them check seats

Singleton as a global, used as a service locator

Registry.get() from anywhere

Hidden dependencies, shared state between tests, one registry for the whole process

God object, in waiting

TripService, where every new rule goes

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.

Java
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";
    }
}
*Trip− name: String− seats: int− signed: List<String>~ name(): String~ sign(String): String~ occupancy(): StringTripBook− trips: Map<String, Trip>~ add(Trip)~ trip(String): TripSignupDesk− book: TripBook~ sign(String, String): String
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

*Trip− name: String− seats: int− signed: List<String>~ name(): String~ sign(String): String~ occupancy(): StringTripBook− trips: Map<String, Trip>~ add(Trip)~ trip(String): TripSignupDesk− book: TripBook~ sign(String, String): String

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 std::vector, whose destructor isn't virtual, to reuse its methods

JavaScript

A module-level object that every file imports and changes: a singleton by another name

Python

Subclassing list or dict for convenience, then finding every list method is public

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

1. Trip.getSigned() returns the trip's own list. Why is that the root of the over-booking?
2. Why is SignupList extends ArrayList an anti-pattern here?
3. The checks recognize Registry as a Singleton. What follows?
4. Which is a reasonable use of a data class with no behavior?

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.