Some designs look reasonable when you first write them. The whole team may even praise them as "clean". A year later they are the source of every slow bug fix. The term anti-patterns describes exactly this kind of design: a common practice that looks appropriate on the surface but does more harm than good. Andrew Koenig coined the term in 1995, inspired by the Design Patterns book, and the book AntiPatterns (1998) popularized it. This article picks three anti-patterns that almost every Java business project has met: God object, anemic domain model, and service locator. For each one, we reproduce the symptoms with code that really ran on JDK 21, measure or demonstrate the damage, then show the fix and the cases where it is not really one of these anti-patterns.

This article builds on the design thread we have followed so far: SOLID principles in Java with practical examples for the principles, Dependency Injection: building an IoC container to understand Spring for wiring the parts together, and DDD in Java: Entity, Value Object and Aggregate for the domain model. The three anti-patterns below are the most common ways of violating what those articles teach.

What anti-patterns are and why we need to name them

According to Wikipedia's entry on anti-patterns, an anti-pattern has two traits. It looks appropriate and effective, but its drawbacks outweigh its benefits, and a different solution exists that is documented, repeatable, and more effective. The second trait matters: naming anti-patterns is only useful when we also know what to replace them with. Otherwise it is just criticism.

Be careful too: anti-patterns are not absolute taboos. Most of them have a context where they are acceptable (a one-off script, a prototype). The sections below always point out that boundary, because without it, "fighting anti-patterns" easily turns into dogma.

The God object anti-pattern: one class that knows and does everything

A God object (also called a God class or Blob) is a class that carries too many responsibilities: pricing, stock deduction, database writes, email sending, invoice rendering, report updates. On refactoring.guru, the Large Class code smell is described as a class with too many fields, methods, or lines of code. The usual cause is that developers find it easier to add a feature to an existing class than to create a new one. Nobody decides to create this anti-pattern; it grows through one "small extra method" at a time.

Symptoms of these anti-patterns in code

A minimal OrderManager example from code/s6-33/Demo1GodObject.java:

static class OrderManager {
    private final Map<String, BigDecimal> stock = new HashMap<>();
    private final List<String> emailOutbox = new ArrayList<>();
    private final List<String> sqlLog = new ArrayList<>();
    private final Map<String, Integer> voucherUsed = new HashMap<>();
    private BigDecimal revenue = BigDecimal.ZERO;

    BigDecimal calculateTotal(List<String[]> lines, String voucher) { ... }  // pricing, promotions
    void reserveStock(String sku, int qty) { ... }                        // inventory
    void saveOrder(String id, BigDecimal total) { ... }                   // database
    void sendConfirmation(String email, String id) { ... }                // email
    String renderInvoiceHtml(String id, BigDecimal total) { ... }         // presentation
    void updateRevenueReport(BigDecimal total) { ... }                    // reporting
    boolean validateVoucher(String voucher) { ... }                       // voucher
}

This class violates the single responsibility principle (the S in SOLID): it has at least six reasons to change. Changing the promotion logic, switching the email provider, or redesigning the invoice template all force edits to the same file, so two people working on two different features hit merge conflicts constantly. Testing is painful too: to check the pricing rules alone, we must build the whole OrderManager with its stock, mailbox, SQL log, voucher counter, and revenue report.

The fix: split by responsibility

The classic fix for God object and similar anti-patterns is Extract Class: move each responsibility into its own class or interface, then let a thin coordinating class wire them together. In the fixed version, Pricing only calculates prices, OrderRepository and Notifier are interfaces for the infrastructure, Discount is a strategy, and PlaceOrder only coordinates:

static class PlaceOrder {   // coordinates only
    PlaceOrder(Pricing p, OrderRepository r, Notifier n) { ... }
    BigDecimal handle(String id, String email, List<Line> lines, Discount d) {
        BigDecimal total = pricing.total(lines, d);
        repo.save(id, total); notifier.orderConfirmed(email, id);
        return total;
    }
}

Actual output (JDK 21.0.12, Apple M4, 16 GB):

Fields and methods (reflection):
OrderManager   fields=5 methods=7
Pricing        fields=0 methods=1
PlaceOrder     fields=3 methods=1
total without discount: 390000
total with 10% off:   351000.00
saved: [DH001=390000, DH002=351000.00], emails sent: [a@example.com/DH001, a@example.com/DH002]
God object pricing: 270000.0 (must initialize stock, outbox, sqlLog, voucherUsed, revenue)

The fixed version is testable with two lambdas: one replaces the repository and one replaces the notifier. Changing the discount rule means passing a different Discount without touching the database or email. The reflection counts only illustrate scale (this example is small; a real God object has dozens of fields and hundreds of methods), but the direction is clear: each new class is small, focused, and easy to read.

How to detect anti-patterns like the God object and when they are acceptable

  • Signs: a class name ending in a generic Manager, Helper, Utils, or Service; files thousands of lines long; several groups of methods that share no fields; a class with imports from different layers (JDBC, mail, and HTML at the same time).
  • The cohesion test: if you can split the fields and methods into disjoint clusters, each cluster is a potential class.
  • Acceptable cases: a main class or composition root that wires everything together (but holds no logic), or throwaway code. A large class is not necessarily a God object if every member serves one concept.

Anemic domain model, one of the classic anti-patterns: domain objects with only getters and setters

Martin Fowler described the anemic domain model anti-pattern on November 25, 2003 in his article AnemicDomainModel. The objects carry business names (Order, Customer) and are fully linked to each other, but have almost no behavior: they are "bags of getters and setters", while the logic lives in services that compute and then update the data. Fowler sees this as a procedural style that goes against the object-oriented principle of binding data with behavior. More importantly, he observes that we pay the full cost of a domain model (O/R mapping, many layers) without getting the benefit, because the complex logic has been moved out of the objects.

Reproducing anti-patterns: rules in the service get bypassed

Demo2Anemic.java puts two designs side by side with the same rule: "a paid order cannot get new lines". The anemic version keeps the rule in OrderService and the data in OrderData with public setters:

static class OrderService {
    void addLine(OrderData o, BigDecimal price) {
        if ("PAID".equals(o.getStatus())) throw new IllegalStateException("Order already paid");
        o.getLines().add(price);
    }
}
...
svc.addLine(data, new BigDecimal("100000"));
data.setStatus("PAID");
svc.addLine(data, new BigDecimal("50000"));            // blocked
data.getLines().add(new BigDecimal("-999999"));        // another piece of code bypasses the service
data.setStatus("PAID"); data.setLines(new ArrayList<>());

The rich-model version puts the rule inside Order itself, has no setters, and returns a read-only copy of the list. Actual output:

== Anemic ==
via service: blocked: Order already paid
bypass with getLines().add(): total = -899999 (PAID order modified, negative price)
empty order still PAID: status=PAID, lines=[]
== Rich ==
addLine after pay: Order already paid
lines().add(): UnsupportedOperationException
pay empty order: Empty order
negative price: Price must be positive
total: 100000

In the anemic version, the rule only works if every caller remembers to go through OrderService.addLine. A single getLines().add(...) in a batch job, a controller, or a test is enough to break it: a paid order gets modified, a negative price appears, and an empty order still carries the PAID status. In the rich version, such anti-patterns are avoided because there is no path other than addLine and pay, so the invalid state cannot exist. This is also the motivation for the Aggregate covered in the DDD article.

Fixing anemic anti-patterns and their pitfalls

  • Move behavior into the object: every state change goes through a method with a business name (pay(), cancel()) instead of setStatus.
  • Remove public setters and do not expose mutable collections: return List.copyOf or a read-only view.
  • Use Value Objects instead of primitive types (money, phone number) so validation happens in the constructor.
  • Keep services in their proper role: according to Fowler, a Service Layer is still valid when it sits on top of a domain model rich in behavior. Evans describes the application layer as thin, coordinating but not holding business rules. The warning only applies when the service owns most of the logic.
  • Do not force it: a pure CRUD application has almost no rules, so getter/setter classes are enough there. DTOs and objects exchanged across a boundary (JSON, messages) are also allowed to be "data bags"; the problem only arises when they are the domain model.

Note: using Lombok @Data on a JPA entity generates setters for every field, which makes an anemic model very easy to create. The library does not create the anti-pattern, but it makes the path there far too easy.

Service locator among the anti-patterns: dependencies hidden inside method bodies

Service locator, a controversial anti-pattern, is a registry that code calls to fetch dependencies: Locator.resolve(Validator.class). It looks like Dependency Injection because it also decouples a class from concrete implementations. But Mark Seemann, in Service Locator is an Anti-Pattern (February 3, 2010), argues that it hides a class's dependencies, shifts errors from compile time to run time, and makes it harder to judge the impact of changes. His example: OrderProcessor has a default constructor and can be created normally, but calling Process blows up because IOrderValidator has not been registered.

Reproducing anti-patterns: errors only appear at run time

Demo3ServiceLocator.java builds exactly that scenario in Java, together with a constructor injection version for comparison:

static class LocatorOrderProcessor {
    String process(String order) {
        if (!Locator.resolve(Validator.class).ok(order)) return "rejected";
        return Locator.resolve(Shipper.class).ship(order);   // dependency buried in the method body
    }
}
static class InjectedOrderProcessor {
    InjectedOrderProcessor(Validator v, Shipper s) { ... }  // dependencies visible in the signature
}
== Service Locator ==
new LocatorOrderProcessor(): succeeds, reports nothing missing
process() at run time: Not registered: Validator
after registering Validator, still fails: Not registered: Shipper
fully registered: shipped DH001
-- static state leaking between tests --
test B (forgot to register, but test A registered earlier): shipped DH002
test B run alone: Not registered: Validator
== Constructor injection ==
missing dependency: fails right at construction (NullPointerException)
fully constructed, test needs no registry: shipped DH003

Three things stand out. First, successful construction says nothing about whether the class is usable; errors arrive one at a time at call time (missing Validator, and after fixing it, missing Shipper). Second, anyone using the class must read the method body to know what to register. Third, and most dangerous for testing, anti-patterns like this make tests order-dependent: the static registry is shared across the whole JVM, so test B stays green even though it forgot to register, as long as test A runs first, then turns red when run alone. Constructor injection is the opposite: dependencies show up in the signature, a missing one fails immediately at construction, and a test only needs to pass lambdas.

A balanced view: Fowler's opinion

The picture is not one-sided. In the classic article Inversion of Control Containers and the Dependency Injection pattern, Fowler argues that for application classes, depending on a locator is "not a big issue", and that testing does not favor injection because both can be replaced if the locator is designed to be replaceable. The real difference he sees is with reusable components that other people deploy: there, even a minimal assumption about the locator is a problem, so injection is better. At the end of that section he writes that the choice between the two matters less than the principle of separating configuration from use.

The two opinions are not as contradictory as they sound. On these anti-patterns, both agree on the principle of separating configuration from use. Seemann objects to locators scattered through business logic; Fowler points out that at the application boundary the impact is small. In Spring, ApplicationContext.getBean(...) is a service locator, and it is acceptable in a few places (startup code, integration with frameworks that do not support injection, a few static utility classes). But if your business service calls getBean inside its own method body, you are in exactly the scenario of the demo above.

Fixing service locator and related anti-patterns

  • Constructor injection for required dependencies, as in the article on IoC containers. Spring encourages this because the class becomes immutable and easy to test.
  • An injected factory or Supplier when you need to create short-lived or conditional objects, instead of passing the whole container.
  • Composition root: only one place (main, configuration) knows the container; everything else does not.

How the three anti-patterns relate to each other

These three anti-patterns are not independent. An anemic system often produces fat services, which are God objects at the service layer: when logic is pulled out of domain objects, it has to go somewhere. A fat service then depends on many things, and to avoid an ugly ten-parameter constructor, people fetch dependencies through a locator or getBean, so the dependencies get hidden. Conversely, fixing one end pulls the others along: when you move behavior back into domain objects, services shrink; when services shrink, constructors naturally get shorter and the temptation to use a locator disappears.

Anti-patternSymptomMeasured harmFix
God objectOne class with many responsibilities and many unrelated fieldsMerge conflicts, tests must build the whole classExtract Class, strategy, thin coordinator
Anemic domain modelEntities with only getters/setters, logic in servicesInvariants bypassed (demo: PAID order modified, negative price)Behavior into objects, remove setters, Value Objects
Service locatorresolve() in method bodies, static registryRun-time errors, order-dependent testsConstructor injection, composition root

How to handle anti-patterns in real code

Dealing with anti-patterns is incremental work. When you meet a 3,000-line God object, do not rewrite it from scratch. Write tests that describe the current behavior, Extract Class one cohesive cluster of fields and methods, rerun the tests, then repeat each time you touch the file for another reason. For an anemic model, pick one frequently violated rule, move it into the object, delete the matching setter, and let the compiler point out every place that bypassed it. For a service locator, start with the class that has the most resolve calls and switch it to a constructor. Measure results with simple numbers such as the number of fields, the number of responsibility groups, and how many times the file was edited in a month (from git log).

Common mistakes when fighting anti-patterns

  • Splitting too much: breaking a class into dozens of one-method classes makes code harder to follow than the God object; split by reason to change, not by line count.
  • Replacing a God object with a new "God service": renaming OrderManager to OrderService without splitting responsibilities changes nothing.
  • A rich model in name only: putting methods in the entity but keeping public setters protects nothing.
  • Banning locator-style anti-patterns absolutely: this ignores the application-boundary case that Fowler points out; the goal is explicit dependencies in business logic.
  • Labeling instead of analyzing: calling everything one of the anti-patterns without pointing to specific harm makes it hard for others to agree.

Checklist for reviewing anti-patterns

  • Which classes have more than one reason to change? Are there field clusters that are not shared with other clusters?
  • Do entities have public setters or return mutable collections? Where do the business rules live?
  • Are there getBean, resolve, or static registry calls in business logic?
  • Does a test have to build many unrelated things to run? Do tests produce different results depending on run order?
  • Is there a test protecting the behavior before the refactor?
  • Is this pure CRUD, where a simple model is enough?

In short, naming anti-patterns is a communication tool for naming a problem and pointing to a way out, not a weapon for mocking code. All three cases share one root: a lack of clear boundaries, whether the boundary of responsibility, of invariants, or of dependencies. When the boundaries are set correctly, the code is easier to change. References: Fowler: AnemicDomainModel, Seemann: Service Locator is an Anti-Pattern, and refactoring.guru: Large Class.

Next article

Knowing how to name harmful designs is only half the job; the other half is writing tests so you can refactor safely without breaking old behavior. Continue with Testing patterns: Test Data Builder, Object Mother and test doubles.