Control without clarity
A God service often indicates that orchestration, policy, domain state changes, and infrastructure concerns were never separated.
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.
| 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 |
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.
A God service often indicates that orchestration, policy, domain state changes, and infrastructure concerns were never separated.
When objects do not protect invariants or express rules, services begin to mimic procedural programming around passive records.
Repositories start validating business rules, controllers begin coordinating workflows, and integrations leak technical concerns upward.
Using String for status, email, money, or country codes leaves validation, meaning, and constraints scattered everywhere.
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.
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.