Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Spotting Tightly-Coupled Code: Smells & Anti-Patterns

Introduction

Welcome to Module 8, where we begin the "Senior Developer Leap." In our previous module, we focused on mastering Agile and project management processes, culminating in a lesson on how to proactively manage project risks. We framed risk management as an essential skill for senior contributors who think beyond immediate tasks. Today, we'll apply that same foresight to the code itself. Poorly structured code is a significant source of technical risk, accumulating "technical debt" that makes every future change slower and more dangerous.

This lesson addresses the first step in managing that debt: learning to see the warning signs. You will learn to identify common code smells and anti-patterns, particularly those that create tightly-coupled components in legacy systems. For a developer with your background in Java and Spring Boot, recognizing these patterns is the foundational skill for refactoring existing applications and architecting robust, maintainable solutions—a core competency on your path to senior-level roles.

1. What Are Code Smells and Anti-Patterns?

It's a common misconception that "bad code" is always code that crashes or produces incorrect results. In reality, some of the most challenging codebases function correctly but are nearly impossible to change safely. This is where the concepts of code smells and anti-patterns become critical.

Code Smells Uncovered: How to Detect and Defuse Technical Debt Before It Grows - IN-COM DATA SYSTEMS

This article from IN-COM DATA SYSTEMS provides an excellent introduction to the concept of code smells and why they are crucial to understand.

First, read the section What Are Code Smells? to grasp the core definition. Note the distinction between a smell and a bug. Then, jump to the section How Code Smells Impact Technical Debt to understand the long-term consequences of ignoring these issues.

As the reading highlights, a code smell is a symptom in the source code that suggests a deeper design problem. The code works, but it's brittle, confusing, or difficult to modify. An anti-pattern is a related concept: it's a commonly used but ultimately counterproductive solution to a recurring problem. For our purposes, we can think of them as two sides of the same coin—indicators that our code's structure is working against us.

Identifying these issues is not about aesthetic preference; it's about risk management at the code level. Every smell you ignore acts like compound interest on your technical debt, making every future feature more expensive and risky to implement.

2. A Catalog of Common Code Smells

Code smells come in many forms. This infographic provides a quick visual summary of some of the most frequent offenders.

This infographic illustrates eight common code smells, such as Large Classes, Feature Envy, and Shotgun Surgery, which are all indicators of underlying design issues.

Let's explore some of the most critical smells, especially those that lead to the tight coupling you've identified as a key challenge in modernizing legacy systems.

Code Smells Uncovered: How to Detect and Defuse Technical Debt Before It Grows - IN-COM DATA SYSTEMS

This section of the same article provides a detailed catalog of common smells with brief examples.

Please read the descriptions for the following smells: Duplicated Code, Long Methods and God Classes, Feature Envy and Data Clumps, and Shotgun Surgery and Divergent Change.

Let's connect these definitions to concrete examples you might encounter in a Java/Spring Boot application.

  • God Class/Long Method: This is one of the most common and damaging smells. A single class or method tries to do everything. This directly violates the Single Responsibility Principle. A controller that validates input, contains complex business logic, calls multiple services, formats the response, and sends notifications is a classic "God Class."

  • Feature Envy: This occurs when a method seems more interested in the data of another class than its own. It's constantly calling getter methods on another object to perform some logic. This smell often indicates that the logic should be moved into the class that owns the data.

  • Shotgun Surgery: You have to make small changes in many different classes to implement one new feature. This is a strong sign that a single responsibility has been split and scattered across your codebase, creating high coupling.

In this next video, renowned software craftsman Sandro Mancuso works with a piece of legacy Java code. Watch how he identifies "hardwired dependencies" and the "Feature Envy" smell.

Testing and Refactoring Legacy Code

This video by Sandro Mancuso provides a masterclass in analyzing and refactoring legacy code.

First, watch the segment on hardwired dependencies, which are a primary cause of tight coupling. Then, jump to the section where he identifies Feature Envy and explains why it's a problem of misplaced behavior.

3. Spring Boot Anti-Patterns

Beyond general code smells, specific frameworks have their own common anti-patterns. Given your experience with Spring Boot, recognizing these will be particularly valuable. These anti-patterns often look like convenient shortcuts but introduce tight coupling that complicates testing and long-term maintenance.

This video provides an excellent tour of common anti-patterns in Spring Boot REST APIs. We will focus on three that are directly related to tight coupling.

Spring Boot REST API Anti-Patterns and Best Practices

This video from SivaLabs provides practical advice on avoiding common pitfalls in Spring Boot applications.

Please watch the following three segments: Using Field Injection: This explains why @Autowired on fields is an anti-pattern and how constructor injection improves testability and clarifies dependencies. Using Entities as Payloads: This covers why you shouldn't expose your JPA entities directly in your API, and how using Data Transfer Objects (DTOs) decouples your API contract from your database schema. Not Using Proper Packaging: This demonstrates why the traditional "package-by-layer" structure can lead to a tangled mess and advocates for a "package-by-feature" approach to improve modularity.

Let's synthesize these anti-patterns:

  • Field Injection (@Autowired on fields): This is a classic anti-pattern because it hides a class's dependencies and makes it difficult to instantiate the class in a unit test without the Spring framework. It tightly couples your class to the dependency injection container. Constructor injection, by contrast, makes dependencies explicit and your classes easy to test.

  • Using JPA Entities as Payloads: Exposing your database entities directly through your REST API is convenient at first, but it tightly couples your external API contract to your internal persistence model. Any change to your database entity (e.g., adding a column) can break your API clients. Using dedicated Data Transfer Objects (DTOs) creates a clean separation.

  • Package-by-Layer: Structuring your project into packages like controller, service, and repository is very common. However, as the video explains, this encourages high coupling within a feature, as components become public and are used indiscriminately by other features. A package-by-feature structure, where all code related to a single feature (e.g., "orders") resides in one package, promotes high cohesion and low coupling.

Recognizing these patterns in your own work is a significant step toward writing code that is truly production-grade and maintainable.

4. How to Spot the Smells

You don't have to find these smells entirely on your own. Modern IDEs and static analysis tools are designed to detect many of them automatically. Tools like SonarLint, which integrates directly into IntelliJ IDEA or VS Code, can provide real-time feedback.

An example of the SonarLint tool within an IDE, flagging a call to `System.gc()` as a code smell and explaining why it's a bad practice. This immediate feedback helps train your instincts.

While tools are helpful, the ultimate goal is to develop your own "nose" for code smells. The real skill lies in exercising judgment—knowing which smells are causing the most pain and need to be addressed, and which are acceptable trade-offs for now.

5. Practical Application: Conduct a Code Smell Review

It's time to apply what you've learned. Your task is to act as a code quality reviewer on your own project.

  1. Choose a class from your current codebase that you are familiar with—a Spring @RestController or @Service is a great candidate.
  2. Review the code specifically looking for the smells and anti-patterns we've discussed.
  3. Identify at least two different smells or anti-patterns.
  4. In your personal notes (e.g., in your Notion or Obsidian knowledge base), document your findings for each one:
    • Smell/Anti-Pattern Name: (e.g., God Class, Field Injection).
    • Location: Briefly describe where it is (e.g., "The processOrder method in OrderController.java").
    • Justification: Explain why it fits the definition. For example: "This OrderController is a God Class because it has over 500 lines and is responsible for request validation, inventory checking, payment processing, and sending email notifications. This violates the Single Responsibility Principle."

This exercise will train you to look at code with a more critical eye, moving your focus from "does it work?" to "is it well-structured and maintainable?"

Conclusion

In this lesson, we have taken the first crucial step in the "Senior Developer Leap" by learning how to diagnose problems in a codebase's structure. You now have a vocabulary to describe and a mental framework to identify common code smells and anti-patterns that lead to tightly coupled, hard-to-maintain legacy systems.

Key Takeaways:

  • Code smells are symptoms of deeper design problems, not bugs. They indicate code that is difficult to change.
  • Common smells like God Classes, Feature Envy, and Shotgun Surgery are red flags for tight coupling and poor separation of concerns.
  • In Spring Boot, anti-patterns like field injection, using entities as API payloads, and package-by-layer are common shortcuts that create long-term maintenance problems.
  • Identifying these issues is the essential first step before any refactoring can begin.

You've learned to spot the "risks" in your code. In our next lesson, we will begin fixing them. We'll dive deep into the SOLID principles, a set of five design principles that provide a powerful toolkit for refactoring smelly code into clean, modular, and testable components.

Can't find a good explanation? Sign up and we'll make it for you

Sign up