Question 18 · Section 18

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...

Language versions: English Russian Ukrainian

🟢 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:

  1. Start: A small service with 3 methods.
  2. Growth: “I’ll just add one more method here, why create a new class?”
  3. 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
Email 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: @Transactional on 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 calls logActivity(), Thread B waits, even though it calls createUser() — they don’t conflict on data, but block on the same monitor.
  • After splitting: Each class has its own monitor. AuditLogger.synchronized and UserService.synchronized don’t block each other.
  • Lock striping: Instead of one synchronized, use ReentrantLock per 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 synchronized contention — 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):

  1. 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).
  2. Week 2: Extracted PaymentValidator (15 methods) — LCOM dropped to 3.1.
  3. Week 3: Extracted PaymentLogger (8 methods), FraudDetector (20 methods) — LCOM 1.8.
  4. Week 4: PaymentProcessor became a facade, delegating to 4 new classes. Clients unchanged.
  5. Week 5: Replaced synchronized with ReentrantReadWriteLock — read operations (check status) no longer block each other.
  6. 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

  1. Separate hot/cold logic — hot classes should be small and JIT-friendly.
  2. Lock granularity — one synchronized on God Object = no deadlock risk, but throughput is killed. Splitting = fine-grained locking.
  3. Context Object instead of shared state — pass immutable context, avoid mutations.
  4. Saga for distributed transactions — after splitting God Object into microservices, @Transactional no longer works.
  5. Progressive refactoring — delegation first (facade), then client migration, then facade removal. No Big Bang.
  6. Characterization tests FIRST — without them, refactoring a God Object is shooting in the dark.
  7. 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 synchronized blocks 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]]