How to Refactor God Object?
Imagine one huge Swiss Army knife with 200 tools. To find the right screwdriver, you have to go through everything: from the bottle opener to the saw. Better to have a set of 10...
🟢 Junior Level
Simple Definition
God Object is a class that “does everything.” It contains hundreds of methods, dozens of fields, and is responsible for many unrelated tasks. This is an anti-pattern because such a class is impossible to read, test, and maintain.
Analogy
Imagine one huge Swiss Army knife with 200 tools. To find the right screwdriver, you have to go through everything: from the bottle opener to the saw. Better to have a set of 10 small knives, each for its own task.
Simple Example
// BAD: God Object — does EVERYTHING
public class UserManager {
public void createUser() { ... }
public void deleteUser() { ... }
public void sendEmail() { ... }
public void generateReport() { ... }
public void connectToDatabase() { ... }
public void validateInput() { ... }
public void logActivity() { ... }
public void exportToPdf() { ... }
// 50 more methods...
}
// GOOD: split by responsibilities
public class UserService { public void createUser() { ... } }
public class EmailService { public void sendEmail() { ... } }
public class ReportService { public void generateReport() { ... } }
public class Logger { public void logActivity() { ... } }
When to Use (rather, when NOT to leave a God Object)
- When a class has > 30 methods — time to think about it.
- When a class has methods for DB, UI, email, and business logic simultaneously.
- When you can’t describe what this class does in one sentence.
🟡 Middle Level
How It Works Internally
God Objects grow evolutionarily. Nobody creates one intentionally — it happens through gradual entropy:
- Start: A small service with 3 methods.
- Growth: “I’ll just add one more method here, why create a new class?”
- Crisis: 200 methods, 50 fields, compilation time grows, tests take minutes.
The LCOM (Lack of Cohesion in Methods) metric formalizes the problem:
- LCOM > 1.0 means the class’s methods are not related to each other — the class is a candidate for splitting.
Decomposition Algorithm
Step 1: Feature Analysis
Group methods and fields by business meaning, without touching the code:
| Group | Methods | Fields | New Class |
|---|---|---|---|
| User CRUD | createUser, deleteUser, updateUser |
userRepository |
UserService |
sendEmail, sendWelcomeEmail, sendResetPassword |
emailClient, emailTemplate |
EmailService |
|
| Logging | logLogin, logError, logAudit |
logWriter |
AuditLogger |
| Reports | generateReport, exportToPdf, exportToCsv |
reportTemplate |
ReportService |
Step 2: Extract Interfaces (ISP)
If the God Object is used by different clients for different purposes, extract narrow interfaces:
public interface UserReader { User findById(Long id); }
public interface UserWriter { void save(User user); }
public interface UserNotifier { void notifyUser(Long userId, String message); }
Each client depends only on the interface it needs, not on the entire monolith.
Step 3: Extract Classes via Delegation
First, make the God Object a facade:
public class UserManager { // former God Object
private final UserService userService = new UserService();
private final EmailService emailService = new EmailService();
// Delegates — keeping clients unchanged for now
public void createUser(User u) { userService.create(u); }
public void sendEmail(String to, String msg) { emailService.send(to, msg); }
}
Then gradually migrate clients to use the new classes directly.
Common Refactoring Mistakes
| Mistake | Solution |
|---|---|
| Shared state: 50 fields used interchangeably by all methods | Pass state as method parameters or create a Context Object |
Transactional integrity: God Object ensured atomicity via synchronized |
After splitting, need a transaction coordinator or Saga pattern (a template for distributed transactions: if a step fails, a compensating action rolls back previous steps) |
| Parasite classes: Extracted a class, but it still depends on 80% of the God Object’s fields | Deepen the analysis — perhaps the extraction was too superficial |
| Big Ball of Mud: Everything is connected to everything, no clear boundaries | Start with tests — if a method can’t be tested without 20 mocks, that’s your cut point |
Approach Comparison
| Approach | Pros | Minuses | When to Apply |
|---|---|---|---|
| Extract Class | Clear separation, each class — one responsibility | Time overhead, need to update all clients | Main strategy |
| Facade | Quickly hides complexity, clients don’t break | Temporary measure — Facade still knows everything | Intermediate stage |
| Mediator | Decouples methods that actively communicated with each other | Adds yet another class | When there’s dense internal communication |
| Strategy | Replaces conditional logic (if/switch) with polymorphism | More classes | When God Object has 50+ if/else branches |
When NOT to Refactor
- Legacy without tests — write characterization tests first, then refactor. Without tests, you won’t know if you broke something.
- End-of-life project — if everything will be deleted in a month, the investment won’t pay off.
- God DTO — if the “God Object” is just a DTO with 100 fields and no behavior, it’s not a design problem, it’s a domain model problem (solved via Bounded Context).
🔴 Senior Level
Deep Internal Implementation
JVM Level: God Object Memory Layout
A large class with 50 fields creates an object with significant overhead:
Object Header: 12 bytes (mark word + class pointer)
50 fields: 50 × 4 bytes (reference) = 200 bytes
Padding: ~4 bytes (padding to 8-byte boundary)
Total: ~216 bytes per instance
After splitting into 10 classes with 5 fields each:
10 × (12 header + 20 refs + 0 padding) = 10 × 32 = 320 bytes
+ 9 × 4 bytes (references between objects) = 36 bytes
Total: ~356 bytes
Overhead: ~140 bytes per instance (65% more). In absolute numbers — negligible for business applications. But in highload with millions of objects — 140 MB per 1 million instances.
Bytecode and JIT
God Object with 200 methods:
- Constant pool grows — more class loading overhead.
- JIT can’t effectively inline methods from a huge class — CPU registers overflow, JIT falls back to interpreter.
- Hot methods (frequently called) “drown” among cold methods — JIT compiler spends compilation budget analyzing the entire class.
After splitting:
- Hot methods in small classes are aggressively inlined.
- JIT compilation budget is spent efficiently.
- CPU instruction cache utilization is higher — small class code fits in L1 cache (32 KB).
Architectural Trade-offs
| Approach | Pros | Cons |
|---|---|---|
| Full decomposition | Each class SRP, easy to test, easy to replace | “Class Explosion” — 50 small classes instead of one, navigation gets harder |
| Partial decomposition | Balance between simplicity and modularity | Boundaries may be blurry |
| Facade + internal classes | Backward compatibility, gradual migration | Facade remains a coupling point |
| Don’t touch | Zero risk of breaking | Technical debt grows, onboarding new developers takes weeks |
Edge Cases
1. Shared Mutable State: God Object with 50 fields used interchangeably by all methods. When splitting, each new field must “belong” to one class.
Solution — Context Object:
public class OrderProcessingContext {
public Order order;
public User customer;
public PaymentMethod payment;
public ShippingDetails shipping;
}
// Passed as a parameter instead of shared state
2. Transactional Integrity After Splitting:
// Was: one synchronized method in God Object
public synchronized void processOrder() {
saveToDb(order); // Repository 1
sendEmail(customer); // Service 2
updateInventory(item); // Service 3
}
After splitting, processOrder must coordinate 3 services. synchronized(this) no longer works.
Solution:
- In a monolith:
@Transactionalon the facade method. - In a distributed system: Saga Pattern with compensating transactions.
3. Circular Dependencies After Splitting:
class UserService { EmailService email; }
class EmailService { UserService user; } // CYCLE!
Occurs when extracted classes reference each other.
Solution: Introduce a third coordinator (UserNotificationOrchestrator) or use events (ApplicationEventPublisher).
4. God Object as a “Util” Class:
public class Utils {
public static void sort(...) { ... }
public static void validate(...) { ... }
public static void format(...) { ... }
public static void encrypt(...) { ... }
}
A static God Object. Impossible to mock, impossible to inherit.
Solution: Split into SortUtils, Validator, Formatter, Encryptor — or better, use ready-made libraries (Guava, Apache Commons).
Performance Implications
| Metric | God Object (200 methods) | After Splitting |
|---|---|---|
| Class loading | 50-100 KB metaspace | 10 × 5 KB = 50 KB metaspace |
| JIT warmup | 30-60 sec (analyzing 200 methods) | 10-20 sec (each class compiled separately) |
| L1 I-cache miss | 15-25% (code doesn’t fit) | 5-8% (small classes are cache-friendly) |
| Benchmark (ops/ms) | ~8,000 (hot path) | ~12,000 (+50% after separating hot/cold logic) |
Hot vs Cold Logic Separation:
- Hot: validation, calculations, serialization — called thousands of times/sec.
- Cold: logging, configuration, metrics — called a few times/sec.
- When they’re in one class, JIT compiles both types into one code blob — CPU cache gets polluted.
- Separation gives hot classes priority in instruction cache.
Memory Implications and GC Impact
- God Object: One large object = one large allocation. If the object is > 512 KB — it goes directly to Old Generation (TLAB overflow), triggering Full GC.
- Split objects: Many small objects = allocation in Eden, fast Young GC collection.
- GC pause: 1 MB God Object = 10-50 ms for Full GC. 100 small objects = 1-3 ms for Young GC.
Thread Safety
- God Object +
synchronized= bottleneck. One monitor blocks ALL methods, even unrelated ones. Thread A callslogActivity(), Thread B waits, even though it callscreateUser()— they don’t conflict on data, but block on the same monitor. - After splitting: Each class has its own monitor.
AuditLogger.synchronizedandUserService.synchronizeddon’t block each other. - Lock striping: Instead of one
synchronized, useReentrantLockper logical data group.
Production War Story
Problem: A PaymentProcessor payment service had 4500 lines, 120 methods, 35 fields. It processed 500 transactions/sec. Under peak load (Black Friday, 5000 transactions/sec) the service degraded: latency grew from 50 ms to 2 sec.
Diagnosis:
- Async Profiler showed 60% of CPU spent on
synchronizedcontention — 15 threads waiting on one monitor. - LCOM = 4.2 (critical value > 1.0) — the class had 4 unrelated method clusters.
- Class file size: 180 KB (norm < 20 KB).
Solution (step by step, without stopping the service):
- Week 1: Wrote characterization tests (tests that capture current system behavior — you don’t know if it’s correct, but you know it works; after refactoring, these tests guarantee behavior hasn’t changed) for every public method (87 tests).
- Week 2: Extracted
PaymentValidator(15 methods) — LCOM dropped to 3.1. - Week 3: Extracted
PaymentLogger(8 methods),FraudDetector(20 methods) — LCOM 1.8. - Week 4:
PaymentProcessorbecame a facade, delegating to 4 new classes. Clients unchanged. - Week 5: Replaced
synchronizedwithReentrantReadWriteLock— read operations (check status) no longer block each other. - Week 6: Migrated clients to direct calls to new services, removed the facade.
Result:
- Latency at 5000 TPS: 50 ms (was 2000 ms — 40x improvement).
- GC pauses: 3 ms (was 50 ms).
- LCOM: 0.4 (norm < 1.0).
Monitoring and Diagnostics
ArchUnit — rules for CI:
@ArchTest
static final ArchRule no_god_objects =
noClasses().that().haveNameNotMatching(".*(Config|Application).*")
.should().haveMethodsMoreThan(30);
SonarQube — metrics:
NCSS(Non-Commenting Source Statements) — target: < 500 per class.Cyclomatic Complexity— target: < 15 per method.LCOM— target: < 1.0.- Rule
S1448: “Classes should not have too many methods” (default threshold: 35).
Structure101 — visualizes connectivity “stars.” God Object appears as a node with maximum degree centrality.
jQAssistant — Neo4j-based analysis:
MATCH (c:Class)
WHERE size((c)-[:DECLARES]->()) > 50
RETURN c.name, size((c)-[:DECLARES]->()) as methodCount
ORDER BY methodCount DESC
IntelliJ IDEA — “Metrics Reloaded” plugin: shows LCOM, coupling, inheritance depth right in the editor.
Best Practices for Highload
- Separate hot/cold logic — hot classes should be small and JIT-friendly.
- Lock granularity — one
synchronizedon God Object = no deadlock risk, but throughput is killed. Splitting = fine-grained locking. - Context Object instead of shared state — pass immutable context, avoid mutations.
- Saga for distributed transactions — after splitting God Object into microservices,
@Transactionalno longer works. - Progressive refactoring — delegation first (facade), then client migration, then facade removal. No Big Bang.
- Characterization tests FIRST — without them, refactoring a God Object is shooting in the dark.
- Bounded Contexts (DDD) (bounded contexts from Domain-Driven Design — dividing the system by business boundaries, where each part has its own model and terminology) — cut along business boundaries, not technical ones.
UserService,PaymentService,NotificationService— these are bounded contexts.
Summary for Senior
- Cut God Object along Bounded Contexts (from DDD), not technical layers.
- Use delegation as an intermediate step — safe migration without downtime.
- Don’t fear “Class Explosion” — 20 small SRP classes are better than one unmanageable monster.
- God Object is not just hard to read — it doesn’t scale well in multi-threaded environments (single monitor bottleneck).
- Characterization tests are the mandatory first step. Without them, you’re not refactoring — you’re guessing.
🎯 Interview Cheat Sheet
Must know:
- God Object — a class with hundreds of methods/fields, does “everything,” LCOM > 1.0
- Algorithm: Feature Analysis → Extract Class (delegation) → ISP interfaces → client migration
- Characterization Tests — mandatory first step for legacy without tests
- Strangler Fig: gradual replacement via facade, no Big Bang
- Hot vs Cold logic separation: hot classes are small for JIT-friendly, cold — can be larger
- Saga Pattern for distributed transactions after splitting God Object into microservices
- Bounded Contexts (DDD) — cut along business boundaries, not technical layers
Frequent follow-up questions:
- How to split shared mutable state? — Context Object: pass state as a parameter instead of shared fields
- What to do with circular dependencies after splitting? — Third coordinator or Event-driven (
ApplicationEventPublisher) - God Object and Thread Safety? — One
synchronizedblocks ALL methods; splitting → fine-grained locking - God Object metrics? — >30 methods, >15 fields, LCOM > 1.0, Cognitive Complexity > 15, >7 dependencies
Red flags (DO NOT say):
- “God Object can be refactored without tests” (shooting in the dark, guaranteed regressions)
- “Split everything at once in one sprint” (progressive refactoring: facade → migration → removal)
- “20 small classes are worse than one God Object” (20 SRP classes are better than one unmanageable monster)
Related topics:
- [[1. What is Single Responsibility principle and how to apply it]]
- [[14. What happens if a class has multiple reasons to change]]
- [[22. What anti-patterns contradict SOLID principles]]
- [[13. How is Single Responsibility related to cohesion]]