Skip to main content
Background Image
  1. Blog/

God Object Detection Done Right: Why SonarQube's Approach Creates False Positives

Author
Glen Baker
Building tech startups and open source tooling
Table of Contents

The God Object Problem
#

A “god object” is a class or module that does too much—it has too many responsibilities, too many methods, too many fields. It violates the Single Responsibility Principle and becomes a maintenance nightmare.

Detecting god objects sounds simple: count the methods and fields, flag anything over a threshold. This is exactly what SonarQube does. And it’s exactly why SonarQube users suffer from alert fatigue.

How SonarQube Detects God Objects
#

SonarQube doesn’t have a unified “god object” rule. Instead, it fires separate alerts for individual threshold violations:

RuleWhat It ChecksDefault Threshold
RSPEC-1448Methods per class20
RSPEC-1820Fields per class15
RSPEC-1200Coupled classes20
RSPEC-3776Cognitive complexity15

Each rule fires independently. No context. No nuance. Just “you exceeded a number.”

The result? Three common false positive patterns that waste developer time.

False Positive #1: The Pure Utility Module
#

Consider this Rust module with scoring configuration:

// src/config/scoring.rs - 72 functions, 609 lines

/// Base complexity weight
pub fn base_complexity_weight() -> f64 { 1.0 }

/// Weight for cognitive complexity
pub fn cognitive_weight() -> f64 { 1.2 }

/// Weight for nesting depth
pub fn nesting_weight() -> f64 { 0.8 }

/// Maximum allowed cyclomatic complexity
pub fn max_cyclomatic() -> u32 { 15 }

/// Default threshold for method count
pub fn default_method_threshold() -> usize { 20 }

// ... 67 more pure configuration functions

What SonarQube says:

❌ Class has 72 methods (threshold: 20) - CRITICAL
❌ File has too many functions - MAJOR

Why it’s wrong:

This isn’t a god object. It’s a cohesive utility module with these characteristics:

  • 97.7% cohesion: Functions call each other, not external code
  • Pure functions: No side effects, no state mutation
  • Single responsibility: All functions serve one purpose—scoring configuration
  • Trivial complexity: Average 1 line per function

Splitting this into 4 files of 18 functions each adds friction with zero benefit.

What debtmap says:

functions                 72 → 18 (pure-weighted)
file cohesion             97.7% [high]

recommendation
  action                  Review 6 detected responsibilities - consider
                          grouping related helpers into submodules

  rationale               Utility module with 72 pure helper functions
                          (weighted: 18). High function count but low
                          effective complexity - verify if detected
                          responsibilities represent distinct concerns

Debtmap recognizes that pure functions without side effects contribute less to god object complexity. The weighted count (18) reflects the actual cognitive load, not the raw count (72).

False Positive #2: The AST Visitor
#

Consider an AST visitor that traverses Rust syntax trees:

// A call graph extractor implementing syn::Visit

pub struct CallGraphExtractor {
    call_graph: CallGraph,
    current_function: Option<FunctionId>,
    type_tracker: TypeTracker,
    // ... 9 more coordination fields
}

impl<'ast> syn::Visit<'ast> for CallGraphExtractor {
    fn visit_expr(&mut self, expr: &'ast Expr) { /* ... */ }
    fn visit_stmt(&mut self, stmt: &'ast Stmt) { /* ... */ }
    fn visit_item_fn(&mut self, item_fn: &'ast ItemFn) { /* ... */ }
    fn visit_item_impl(&mut self, item_impl: &'ast ItemImpl) { /* ... */ }
    fn visit_local(&mut self, local: &'ast Local) { /* ... */ }
    fn visit_item_mod(&mut self, item_mod: &'ast ItemMod) { /* ... */ }
    // ... 12 more visit_* methods required by syn::Visit trait
}

impl CallGraphExtractor {
    pub fn new(file: PathBuf) -> Self { /* ... */ }
    pub fn extract(&mut self, file: &syn::File) -> CallGraph { /* ... */ }
    fn handle_call_expr(&mut self, call: &ExprCall) { /* ... */ }
    fn handle_method_call(&mut self, call: &ExprMethodCall) { /* ... */ }
    // ... 14 more self-chosen helper methods
}

What SonarQube says:

❌ Class has 32 methods (threshold: 20) - CRITICAL
❌ Class has 12 fields (threshold: 10) - MAJOR
❌ Class has high fan-out coupling - MAJOR

Recommendation: "Split into smaller classes"

Why it’s wrong:

18 of those 32 methods are trait-mandated. The syn::Visit trait requires a visit_* method for each AST node type. You can’t “extract” them—the trait contract demands they exist on this type.

What debtmap says:

methods                   32 (18 trait-mandated, 14 extractable)
implements                syn::Visit (18 methods)

recommendation
  action                  Consider grouping 14 extractable methods by
                          responsibility

  rationale               18 of 32 methods are trait-mandated
                          (non-extractable). Implements: syn::Visit (18).
                          Focus refactoring on the 14 self-chosen methods.

Debtmap distinguishes between:

  • Trait-mandated methods: Required by interface contract, non-extractable
  • Self-chosen methods: Author’s design choice, potentially extractable

The recommendation targets only the actionable 14 methods, not the 18 you can’t change.

False Positive #3: The Builder Pattern
#

Consider a configuration builder:

pub struct AnalysisConfigBuilder {
    // 25 optional configuration fields
    output_dir: Option<PathBuf>,
    max_workers: Option<usize>,
    timeout_secs: Option<u64>,
    log_level: Option<LogLevel>,
    cache_dir: Option<PathBuf>,
    include_patterns: Option<Vec<String>>,
    exclude_patterns: Option<Vec<String>>,
    threshold_complexity: Option<u32>,
    threshold_nesting: Option<u32>,
    // ... 16 more configuration options
}

impl AnalysisConfigBuilder {
    pub fn new() -> Self { /* ... */ }

    // 25 builder methods - one per field
    pub fn output_dir(mut self, dir: PathBuf) -> Self {
        self.output_dir = Some(dir);
        self
    }

    pub fn max_workers(mut self, n: usize) -> Self {
        self.max_workers = Some(n);
        self
    }

    // ... 23 more identical builder methods

    pub fn build(self) -> Result<AnalysisConfig> { /* ... */ }
}

What SonarQube says:

❌ Class has 26 methods (threshold: 20) - CRITICAL
❌ Class has 25 fields (threshold: 15) - CRITICAL
❌ Data class smell detected - MAJOR

Recommendation: "Refactor to reduce method and field count"

Why it’s wrong:

This is the builder pattern—a well-established idiom. Each field needs a setter method. The 25 methods aren’t 25 responsibilities; they’re one responsibility (configuration building) expressed 25 times.

What debtmap says:

methods                   26 → 5 (pure-weighted)
responsibilities          1 (construction)

recommendation
  action                  Builder pattern detected - no action needed

  rationale               26 methods classified as construction/accessor
                          pattern. Single responsibility: configuration
                          building. Structure is idiomatic.

Debtmap’s accessor detection recognizes builder methods and weights them at 0.2 instead of 1.0. The effective method count (5) reflects the actual architectural complexity.

The Fundamental Difference
#

SonarQube’s approach:

IF methods > 20 THEN alert("too many methods")
IF fields > 15 THEN alert("too many fields")
IF coupling > 20 THEN alert("too much coupling")

Three independent checks. No understanding of why the numbers are high.

Debtmap’s approach:

// Unified analysis considering multiple factors
let weighted_methods = apply_weights(methods, |m| {
    if is_trait_mandated(m) { 0.1 }
    else if is_accessor(m) { 0.2 }
    else if is_pure_function(m) { 0.2 }
    else { 1.0 }
});

let cohesion = calculate_cohesion(internal_calls, external_calls);
let responsibilities = detect_responsibilities(method_names);

// Combined score with context
let score = calculate_unified_score(
    weighted_methods,
    fields,
    responsibilities,
    cohesion,
    complexity
);

// Contextual recommendation
let recommendation = if cohesion > 0.9 {
    "Cohesive utility module - verify if split is needed"
} else if trait_mandated_ratio > 0.5 {
    format!("Focus on {} extractable methods", extractable_count)
} else {
    suggest_splits(responsibilities)
};

What Debtmap Analyzes That SonarQube Doesn’t
#

AnalysisSonarQubeDebtmap
Raw method count
Raw field count
Weighted method count
Cohesion analysis
Responsibility detection
Trait-mandated methods
Pure function detection
Accessor/boilerplate detection
Contextual recommendations
Split suggestions

Real-World Impact
#

In debtmap’s own codebase analysis:

SonarQube-style detection would flag:

  • src/config/scoring.rs (72 functions) - False positive: Pure utility module
  • src/analyzers/call_graph/mod.rs (32 methods) - Partially false: 18 are trait-mandated
  • src/extraction/adapters/god_object.rs (45 functions) - False positive: Functional decomposition

Debtmap detection flags:

  • Files with low cohesion and high responsibility count
  • Structs where extractable methods exceed thresholds
  • Modules with mixed concerns that would benefit from splitting

The difference: actionable recommendations vs noise.

Try It Yourself
#

# Install debtmap
cargo install debtmap

# Analyze a Rust project
debtmap analyze /path/to/project

# See the difference in god object detection
debtmap analyze /path/to/project --filter god_object

Debtmap is open source: github.com/iepathos/debtmap

Conclusion
#

SonarQube’s threshold-based rules were designed for a simpler time—when 20 methods genuinely meant trouble. Modern codebases use patterns (visitors, builders, functional decomposition) that legitimately produce many methods without creating maintenance problems.

The question isn’t “how many methods?”

The question is “how many responsibilities, and can you extract them?”

Debtmap answers the right question.

Related

Refactoring a God Object Detector That Was Itself a God Object
Automating Documentation Maintenance with Prodigy: A Real-World Case Study
Debtmap - Rust Technical Debt Analyzer
Three Patterns That Made Prodigy's Functional Migration Worth It
Stillwater Validation for Rustaceans: Accumulating Errors Instead of Failing Fast
Premortem vs Figment: Configuration Libraries for Rust Applications
Mermaid-Sonar: Detecting Hidden Complexity in Diagram Documentation