Java Omnibus
OOAD & Architecture / Common design failures in real projects
Failure Signatures

The fastest way to improve design judgment is to learn what failure looks like before it becomes expensive.

Real Java systems rarely fail because nobody knew a pattern name. They fail because responsibilities drift, rules scatter, boundaries leak, and a codebase slowly becomes harder to trust. This page focuses on the design failures that show up repeatedly in long-lived systems.

Overview

Five common failures worth learning to recognize early

Failure Typical signal What it usually points to
God service / blob One class coordinates everything Responsibilities never received proper boundaries
Anemic domain model Entities are mostly getters and setters Business rules live far from the concepts they govern
Leaky layers Controllers, repositories, and integrations all know business decisions Dependency direction is unclear
Primitive obsession Strings and numbers represent rich business concepts everywhere Modelling is too shallow
Change amplification One business rule requires many unrelated edits Coupling is too high and cohesion is too low
Java Example

How failure looks in an Order Management service

Java example: a service with too many reasons to change
public class CheckoutService {
    public void checkout(CheckoutRequest request) {
        validateCustomer(request.customerId());
        validateAddress(request.shippingAddress());
        calculateDiscounts(request);
        reserveInventory(request.items());
        authorizePayment(request.paymentMethod(), request.total());
        saveOrder(request);
        createShipment(request);
        sendEmail(request.customerEmail());
        publishMetrics(request);
    }
}

This kind of class often appears reasonable at first. Over time, though, every new rule arrives in the same place: pricing, risk checks, stock handling, payment retries, tax logic, shipment exceptions, and customer notifications. Eventually the service becomes the system.

Concept Breakdown

What each failure really means

God service

Control without clarity

A God service often indicates that orchestration, policy, domain state changes, and infrastructure concerns were never separated.

Anemic model

Data without behavior

When objects do not protect invariants or express rules, services begin to mimic procedural programming around passive records.

Leaky layering

Everything knows too much

Repositories start validating business rules, controllers begin coordinating workflows, and integrations leak technical concerns upward.

Primitive obsession

Weak modelling

Using String for status, email, money, or country codes leaves validation, meaning, and constraints scattered everywhere.

Java Example

Primitive obsession in the same domain

Java example: stringly-typed domain model
public record PaymentRequest(
    String orderId,
    String customerEmail,
    String currency,
    BigDecimal amount,
    String paymentMethod,
    String status
) {}

This shape looks convenient but it pushes meaning outward. Every caller now needs to know what counts as a valid email, status, payment method, or currency. OOAD pushes these concepts back into the model where they can defend themselves.

Step-by-Step Explanation

How to read these failures during code review

  1. Ask which class owns the decision, not just which class executes the line.
  2. Check whether a change request will likely touch more than one layer for the same business reason.
  3. Look for repeated validation or conversion logic around strings, maps, and loose primitives.
  4. Notice when object names suggest meaning, but behavior still lives elsewhere.
  5. Look for classes that become mandatory transit points for every workflow.
Real-World Usage

Where these failures usually surface first

  • Checkout and payment orchestration in e-commerce systems
  • Order status transitions spread across controllers, schedulers, and repositories
  • Notification rules duplicated across synchronous and asynchronous flows
  • Shared DTOs reused as both transport models and domain models
  • Controller layers making policy decisions because no better abstraction was introduced
Trade-Offs / When Not To Overreact

Not every simple service is a design failure

Small, stable, low-complexity use cases can stay simple for a long time. The problem is not simplicity. The problem is when change pressure rises and the design has no good seams. Refactoring should be proportional to actual complexity, not driven by pattern enthusiasm.

Common Mistakes

Ways teams often respond badly to these failures

  • Adding more layers without redistributing responsibility
  • Introducing patterns as wrappers around the same unclear model
  • Renaming services and controllers without rethinking object roles
  • Moving logic to utilities instead of modelling meaningful domain concepts
Related Pages

Related concepts and what to read next


Next Recommended Pages