God class anti-pattern
Learn why a class that knows too much and does too many unrelated things is hard to test, extend, and maintain, and how to break it apart using Single Responsibility and Extract Class.
A god class is a class that has accumulated several unrelated responsibilities, dependencies, and reasons to change. It is not defined by a particular line count; the key problem is that one class becomes the place where many kinds of work are added and understood.
TL;DR
- A god class accumulates too many responsibilities because it already "has all the data." New features tend to land there, and the class grows until its boundaries are hard to explain.
- Merge conflicts and test setup become painful because unrelated features touch the same file and pull in different dependencies.
- The fix is Extract Class: identify cohesive clusters of methods, move each cluster into its own class, and wire them through a thin orchestrating facade.
- A useful diagnostic: can you explain the class in one sentence without listing several unrelated jobs? If not, inspect its responsibilities and cohesion.
- LCOM4 (Lack of Cohesion of Methods) can provide a useful automated signal, but it should support design judgment rather than decide by itself that a class must be split.
The Problem
Six months into a startup, the OrderService looks like this:
// God class: 5 unrelated responsibilities, 18 methods, 2000+ lines
public class OrderService {
private final JdbcTemplate db;
private final PaymentGateway gateway;
private final EmailClient emailClient;
private final InventoryRepo inventoryRepo;
private final ReportEngine reportEngine;
private final AddressValidator addressValidator;
// Order lifecycle
public Order placeOrder(Cart cart) { /* ... */ }
public void cancelOrder(String orderId) { /* ... */ }
public List<Order> getOrderHistory(String userId) { /* ... */ }
// Inventory management
public int checkStock(String productId) { /* ... */ }
public void reserveStock(String productId, int qty) { /* ... */ }
public void releaseStock(String productId, int qty) { /* ... */ }
// Payment processing
public Payment chargeCard(String orderId, BigDecimal amount) { /* ... */ }
public void refundPayment(String paymentId) { /* ... */ }
public PaymentStatus getPaymentStatus(String paymentId) { /* ... */ }
// Notifications
public void sendConfirmationEmail(Order order) { /* ... */ }
public void sendShippingUpdate(Order order, String tracking) { /* ... */ }
// Reporting
public Report getMonthlySalesReport(YearMonth month) { /* ... */ }
public Report getInventoryReport() { /* ... */ }
// Validation
public boolean validateAddress(Address address) { /* ... */ }
public boolean validatePaymentMethod(Card card) { /* ... */ }
}
In this example, each new feature adds another method and tests may require mocking several dependencies. A bug in getMonthlySalesReport can be harder to isolate when reporting and inventory share the same class-level state or error-handling paths. The payments developer must understand inventory logic to make a safe change.
The pattern often starts small and grows silently until it is hard to explain what the class does in one sentence. Because unrelated changes share one file and one set of dependencies, parallel work and review become more difficult.
The fan-out tells the story: one class talks to six external systems. Three developers working on unrelated features all modify the same file.
The test suite is equally painful. Writing a unit test for cancelOrder() may require constructing mocks for the payment gateway, email client, inventory repository, and other collaborators even though cancellation only touches the database. The setup obscures the small behavior being tested.
When unrelated collaborators are needed to test a small behavior, the class may have too many responsibilities. Test friction is a useful symptom, not a mathematical definition.
So how do you fix it when the class already exists and half the company depends on it? The answer is incremental extraction, not a rewrite.
Why It Happens
God classes do not start large. They start with one class doing one thing correctly. Then convenience takes over.
- Data gravity. "The payment data is already here in OrderService, I'll add the charge method here." Convenience beats structure every time.
- Fear of new files. Creating a new class feels like over-engineering. Adding a method to an existing class feels pragmatic.
- Unclear ownership. No team owns "notification logic," so it lands wherever the triggering code lives.
- Sprint pressure. The fastest path to ship a feature is adding it where the data already exists. Refactoring is a separate ticket that never gets prioritized.
Each decision is locally reasonable. In aggregate, they create a class that cannot be changed safely.
The pattern is self-reinforcing. The bigger the class gets, the harder it is to extract a responsibility when methods depend on shared state. That makes the existing class look like the easiest place for the next feature as well.
The workaround trap
As a god class grows, developers may work around it by copy-pasting methods into new services or wrapping it in a helper that adds another layer of indirection. Both approaches can preserve the original coupling and duplicate bugs instead of restoring clear ownership.
The cohesion metric that catches this
LCOM4 (Lack of Cohesion of Methods, version 4) counts connected components in a graph where methods are nodes and edges connect methods that share a field. LCOM4 = 1 indicates one connected method component; a higher value suggests disconnected clusters that may deserve extraction. Tools such as SonarQube, JDepend, and IDE analysis features can compute cohesion metrics, but their exact interpretation depends on the implementation.
How to Detect It
| Signal | Threshold | How to Check |
|---|---|---|
| Method count | A growing public surface with unrelated operations | IDE structure view or grep -c "public.*(" ClassName.java |
| Constructor dependencies | Several dependencies from unrelated domains | Count @Inject or constructor parameters and group them by responsibility |
| Lines of code | Size makes the class hard to navigate or review | wc -l ClassName.java; treat size as context, not a verdict |
| LCOM4 | Multiple disconnected method clusters | SonarQube, JDepend, IntelliJ Analyze |
| Merge conflict frequency | Repeated conflicts on unrelated changes | git log --oneline --diff-filter=M -- OrderService.java and review the reasons |
| Import count | Unrelated packages from several domains | Count imports and group them by the work they support |
| Test setup lines | More mock setup than assertions | Review test files for the class |
| Class description | Needs "and" to explain | Ask someone: "what does this class do?" |
There is no universal cutoff for a god class. A class that shows several signals deserves a closer look at its responsibility boundaries, but a small facade may legitimately have many collaborators and a data-heavy class may score poorly for reasons unrelated to responsibility.
Change history can add evidence. Frequent edits by unrelated features, repeated merge conflicts, or tests that need many unrelated collaborators all suggest that the class is carrying too much shared ownership.
The Fix
The pattern is Extract Class: identify clusters of related methods, move each cluster into its own class, and replace the god class with a thin facade that delegates.
The process works in four steps:
- Map the method clusters. Group methods by which fields they access. Methods that share fields belong together.
- Extract one cluster at a time. Start with the most isolated cluster (fewest dependencies on shared state).
- Replace internal calls with delegation. The god class temporarily delegates to the extracted class.
- Introduce the facade. Once all clusters are extracted, replace the god class with a thin coordinator.
Each class has one reason to change. The CheckoutFacade orchestrates but contains no business logic of its own.
public class OrderProcessor {
private final JdbcTemplate db;
public OrderProcessor(JdbcTemplate db) {
this.db = db;
}
public Order placeOrder(Cart cart) {
Order order = Order.from(cart);
db.update("INSERT INTO orders (id, total, status) VALUES (?, ?, ?)",
order.id(), order.total(), "PENDING");
return order;
}
public void cancelOrder(String orderId) {
db.update("UPDATE orders SET status = 'CANCELLED' WHERE id = ?", orderId);
}
public List<Order> getHistory(String userId) {
return db.query("SELECT * FROM orders WHERE user_id = ?",
Order::fromRow, userId);
}
}
The key design principles at work:
- Single Responsibility Principle: each class has one reason to change. Payment gateway updates only affect
PaymentProcessor. - Facade pattern:
CheckoutFacadeprovides a simple interface without containing logic itself. - Dependency Injection: each class declares only the dependencies it needs. Focused classes can usually be tested with a much smaller fixture than the original god class.
Facade as migration bridge
During refactoring, the facade keeps existing callers working while you extract responsibilities one at a time. Old code calls the facade. New code calls the focused classes directly. Once all callers migrate, you can remove the facade if it adds no value.
Costs and Trade-offs
| Dimension | Impact |
|---|---|
| Team velocity | Unrelated changes are more likely to collide in the same file and require broader review. |
| Test reliability | Tests are slow (mock setup), brittle (shared state), and incomplete (too many paths). |
| Bug isolation | It is harder to trace which responsibility caused a failure when many domains share state and control flow. |
| Onboarding time | New developers must understand unrelated domains before making a change safely. |
| Deployment risk | A change in one responsibility can require regression coverage for behavior that shares the same class. |
| Code review time | Reviewers must reason about unrelated dependencies and side effects in the same file. |
These costs are contextual rather than fixed. They become visible when the class changes frequently, multiple people work in it, or tests need collaborators from unrelated domains.
When the Pattern Is Fine
- Prototypes and short-lived experiments. If the code is deliberately temporary, splitting classes can add overhead before the design is known. Revisit the structure if the experiment becomes maintained product code.
- Thin orchestrators. A facade that coordinates five services but contains no business logic is not a god class, even if it imports many dependencies. The test is whether it has logic, not whether it has references.
- Framework-imposed entry points. Some frameworks force a single controller or handler class. If the framework requires it, delegate immediately to focused classes rather than fighting the framework.
- Small, short-lived code. If the code is a throwaway prototype or a small private utility with one clear purpose, splitting it can add ceremony without benefit. Revisit the design when responsibilities, callers, or change frequency grow.
The bottom line: god classes are bad because of their effects (merge conflicts, untestable code), not because of some abstract rule. If the effects are not present yet, the class is not a problem yet.
30-Second Explanation
A god class has several unrelated reasons to change. It often has many dependencies, methods that use different subsets of its fields, and tests that need collaborators unrelated to the behavior under test. Split it by cohesive responsibility with Extract Class; keep a thin facade only when it provides useful coordination or a migration boundary.
5-Minute Explanation
Describe the class in terms of responsibilities, not size. In the example, order lifecycle, inventory, payments, notifications, reporting, and validation have different data, dependencies, and change drivers. Group methods by the fields and collaborators they use, then extract one coherent cluster at a time.
Make the migration safe by preserving the existing entry point as a delegating facade while callers move to focused classes. Do not extract one method per class or create a new Manager with the same mixed responsibilities. After extraction, check whether each class has a clear purpose, a small dependency surface, and tests that exercise its own behavior.
LCOM4 can help reveal disconnected method clusters, and change history can show whether unrelated work repeatedly collides in the file. Neither replaces judgment: a facade may coordinate many collaborators legitimately, while a small class can still have two unrelated responsibilities.
Use this flowchart as a mental model. Start with responsibility and dependency clusters, then use cohesion metrics and change history as supporting evidence.
Common Mistakes
| Mistake | Why It Fails | Better Approach |
|---|---|---|
Extracting by technical layer (all validators in ValidationUtils) | Groups unrelated logic. Address validation and payment validation have nothing in common. | Extract by domain: address validation lives with address logic, payment validation with payment logic. |
| Creating a "manager" class that renames the god class | OrderManager with 18 methods is the same problem with a new name. | Each extracted class must have 1 responsibility, not just 1 name. |
| Splitting too aggressively (1 method per class) | Kills readability and scatters one responsibility across needless indirection. | Group methods by cohesion; the resulting class size should follow the responsibility rather than a fixed method count. |
| Big-bang refactor in one PR | Makes review, rollback, and regression diagnosis harder. | Extract one responsibility per change when practical. Use the facade as a bridge during migration. |
| Skipping the facade | Callers that used one class now call four. API surface explodes. | The facade preserves the old API while delegates do the work. Remove it once callers adapt. |
Test Your Understanding
Quick Recap
- A god class accumulates responsibilities because it has the data and adding a method is easier than creating a class.
- Symptoms include unrelated public methods, dependencies from several domains, disconnected method clusters, frequent merge conflicts, and broad test setup.
- The one-sentence test: if you need "and" to describe the class, it has too many responsibilities.
- Fix with Extract Class: identify cohesive method clusters, move each to its own class.
- Use the Facade pattern as a transitional bridge that preserves the old API while delegating to focused classes.
- Each extracted class should have one reason to change and one dependency to mock in tests.
- Refactor incrementally (one responsibility per PR), not as a big-bang rewrite.
- Measure success with cohesion, dependency size, change patterns, and focused tests; LCOM4 is supporting evidence rather than a target to optimize blindly.
Related Concepts
- SOLID principles β especially Single Responsibility and Dependency Inversion, which guide responsibility boundaries and dependency direction.
- Facade pattern β provides a stable coordination surface during an incremental extraction.
- Anemic domain model β a different imbalance where domain objects have too little behavior and services do too much of the work.
- Singleton overuse β can make a god class's shared state and dependencies harder to isolate in tests.
Related Articles
Learn why separating all business logic from domain objects into service classes produces procedural code disguised as OOP, and how a rich domain model fixes it.
Learn why overusing singletons creates hidden global state, makes code untestable, introduces threading hazards, and how dependency injection replaces them cleanly.