God class anti-pattern
Learn why a class that knows everything and does everything is the hardest to test, extend, and maintain, and how to break it apart using Single Responsibility and Extract Class.
TL;DR
- A god class accumulates too many responsibilities because it already "has all the data." Every new feature lands there, and the class grows until no single developer understands it.
- Merge conflicts spike because unrelated features touch the same file. Test setup becomes a nightmare of 15+ mocked 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.
- The diagnostic: can you explain the class in one sentence without using "and"? If you need "and" more than once, it has too many responsibilities.
- LCOM4 (Lack of Cohesion of Methods) above 3 is a reliable automated signal that a class should 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) { /* ... */ }
}
Every new feature adds another method. Every test requires mocking six dependencies. A bug in getMonthlySalesReport can silently touch reserveStock because they share mutable state through this.db. The payments developer must understand inventory logic to make a safe change.
I've seen this exact class in three different codebases. It always starts small and grows silently until a new hire asks "what does this class do?" and nobody can answer in one sentence.
The merge conflict math alone kills velocity: if three developers push changes to the same file in one sprint, you get at least two merge conflicts. Multiply that across 26 sprints per year and the god class becomes the single largest source of wasted engineering time.
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() requires constructing a JdbcTemplate mock, a PaymentGateway mock, an EmailClient mock, and four more, even though cancellation only touches the database. Mock setup takes 50 lines. The actual assertion takes 3.
That ratio (50:3 setup-to-assertion) is the clearest code smell of a god class. When the test setup is longer than the test, the class under test has too many responsibilities.
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 tragedy is that the god class is self-reinforcing. The bigger it gets, the harder it is to extract anything from it, because every method depends on shared state. So the next feature lands there too.
The Big Ball of Mud effect
Once a god class exceeds 1000 lines, teams stop trying to understand it and start working around it. They copy-paste methods into new services, duplicating bugs. Or they wrap the god class in a "helper" that just adds another layer of indirection. Both approaches make the codebase worse.
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 means the class is cohesive. LCOM4 = 5 means you have five classes pretending to be one. Tools like SonarQube, JDepend, and IntelliJ's Analyze menu compute this automatically.
How to Detect It
| Signal | Threshold | How to Check |
|---|---|---|
| Method count | 10+ public methods | IDE structure view or grep -c "public.*(" ClassName.java |
| Constructor dependencies | 5+ injected dependencies | Count @Inject or constructor parameters |
| Lines of code | 500+ lines | wc -l ClassName.java |
| LCOM4 | 3+ | SonarQube, JDepend, IntelliJ Analyze |
| Merge conflict frequency | 3+ conflicts/sprint on one file | git log --oneline --diff-filter=M -- OrderService.java |
| Import count | 5+ unrelated packages | Count import statements touching different domains |
| 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?" |
If a class hits 3 or more of these signals, it is almost certainly a god class.
The checkout is straightforward: run git log --format='%H' --diff-filter=M -- OrderService.java | wc -l to see how many commits modified the file in the last quarter. If the number exceeds the total number of features shipped, the class is a merge-conflict magnet.
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 exactly the dependencies it needs. Test setup drops from 6 mocks to 1.
For your interview: say "I'd use Extract Class to split by responsibility, then wire them through a facade." That is the complete answer.
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.
Severity and Blast Radius
| Dimension | Impact |
|---|---|
| Team velocity | Merge conflicts on every sprint. 2-3 devs blocked per week on the same file. |
| Test reliability | Tests are slow (mock setup), brittle (shared state), and incomplete (too many paths). |
| Bug isolation | Cannot trace which responsibility caused a failure without reading 2000+ lines. |
| Onboarding time | New developers take 2-3 weeks to understand the class before making safe changes. |
| Deployment risk | Every deployment touches the god class, so every deployment is a full regression risk. |
| Code review time | Reviewers must understand all 5 domains to approve a change in one. Reviews take 3x longer. |
The velocity impact compounds over time. My experience: teams with a god class ship 30-40% slower than teams that decomposed early. The merge conflict overhead alone costs 2-4 hours per developer per sprint. Over a year, that is 50-100 hours of wasted engineering time per developer.
When It's Actually OK
- Prototypes and hackathons. If the code will be rewritten in two weeks, splitting classes adds overhead with no payoff. Ship it, learn, throw it away.
- 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.
- Early-stage startups with one developer. If you are the only developer and the class has fewer than 300 lines, the overhead of managing five files exceeds the benefit. Revisit when the team grows or the class crosses 500 lines.
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.
How This Shows Up in Interviews
Scenario 1: Code review. The interviewer shows you a 500-line class and asks "what's wrong?" Name the god class anti-pattern, point to the LCOM4 signal (methods that don't share fields), and propose Extract Class. Mention the one-sentence test.
Scenario 2: Design from scratch. You're designing an e-commerce checkout. Show decomposed classes from the start: "I'll have an OrderProcessor for lifecycle, a PaymentProcessor for charges, and a NotificationSender for emails. They're wired through DI." This signals SRP without being asked about it.
Scenario 3: Refactoring legacy code. "How would you refactor this monolith class?" Walk through: identify responsibility clusters, extract one at a time behind an interface, use the facade as a transitional bridge. Mention that you'd refactor incrementally (one class per PR) rather than a big-bang rewrite.
The most common mistake in interviews is proposing a complete rewrite. Interviewers want to see incremental, safe migration.
The one-sentence test
When an interviewer asks you to evaluate a class, start with: "Can I describe this class in one sentence without using 'and'?" If you cannot, say "this has multiple responsibilities and should be decomposed." It is a crisp, memorable signal.
My recommendation: use this flowchart as a mental model. Start with method count (easiest to see), escalate to LCOM4 (most precise), and mention merge conflict frequency as the business-visible symptom.
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. 30 tiny classes are harder to navigate than 5 focused ones. | Target 3-7 methods per class, grouped by cohesion. |
| Big-bang refactor in one PR | Merge conflict nightmare. High risk of regressions. | Extract one responsibility per PR. 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: 10+ methods, 5+ dependencies, LCOM4 above 3, frequent merge conflicts.
- 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 LCOM4: after extraction, each class should have LCOM4 = 1.