LearnAI ToolsCareerPractice BuildsPlayContact
Lesson 2920 min read

Common Mistakes & Best Practices

A consolidated look at the most common Spring mistakes - field injection, @Autowired overuse, and package structure - and how to avoid them.

Introduction

You have now touched most of core Spring - IoC, beans, configuration, AOP, MVC, and JDBC. Before the final project, this lesson pulls together the mistakes that show up most often in real Spring codebases, particularly around dependency injection style and project structure, and lays out the conventions that keep a Spring application maintainable as it grows.

What You Will Learn
  • Why constructor injection is preferred over field injection.
  • Why relying on @Autowired everywhere is a design smell.
  • Recommended package structure for a layered Spring application.
  • A checklist of other common Spring pitfalls.

Field Injection vs Constructor Injection

Field injection (@Autowired directly on a field) is compact but hides a class's real dependencies, makes the class impossible to instantiate without reflection or a Spring container (so plain-Java unit testing without Mockito's field tricks is harder), and allows a bean to exist in a half-broken state with a null dependency if wiring goes wrong. Constructor injection makes dependencies explicit, required, and final.

// Discouraged: field injection
@Service
public class BookService {
@Autowired
private BookRepository bookRepository;
}
// Preferred: constructor injection
@Service
public class BookService {
private final BookRepository bookRepository;
public BookService(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
}
@Autowired Is Optional Here

With a single constructor, Spring 4.3+ implicitly uses it for dependency injection - you do not even need to write @Autowired on the constructor. It only becomes required if a class has more than one constructor.

Overusing @Autowired

A related smell is a class that injects far more dependencies than it should need - five, eight, ten beans autowired into one service. That usually signals the class is doing too much and should be split, following the same single-responsibility thinking you would apply to any large Java class.

// A warning sign: too many responsibilities in one place
@Service
public class OrderService {
public OrderService(OrderRepository orderRepository,
PaymentGateway paymentGateway,
InventoryService inventoryService,
EmailService emailService,
ShippingService shippingService,
AuditLogService auditLogService,
DiscountEngine discountEngine) {
// ...
}
}

A constructor with seven dependencies is not a Spring problem - it is a design problem that constructor injection simply makes visible. That visibility is a feature: it is much easier to notice "this class does too much" from a bloated constructor signature than from a pile of hidden @Autowired fields.

Package Structure Conventions

Most Spring projects converge on one of two structures: packaging by layer (all controllers together, all services together, all repositories together) or packaging by feature (each feature folder contains its own controller, service, and repository). Packaging by layer is common and easy to follow in smaller applications; packaging by feature scales better as an application grows, since related code stays together.

// Package by layer
com.example.bookstore
├── controller
│ ├── BookController.java
│ └── OrderController.java
├── service
│ ├── BookService.java
│ └── OrderService.java
├── repository
│ ├── BookRepository.java
│ └── OrderRepository.java
└── model
├── Book.java
└── Order.java
// Package by feature
com.example.bookstore
├── book
│ ├── BookController.java
│ ├── BookService.java
│ ├── BookRepository.java
│ └── Book.java
└── order
├── OrderController.java
├── OrderService.java
├── OrderRepository.java
└── Order.java
StructureGood Fit For
Package by layerSmaller applications, teams new to Spring
Package by featureLarger applications with many independent features

Other Common Pitfalls

A few more issues show up repeatedly across Spring codebases, independent of injection style or package layout.

  • Putting business logic in controllers instead of the service layer.
  • Reusing JPA/JDBC entity classes directly as API request/response bodies, coupling the database schema to the public API.
  • Catching exceptions too broadly and losing information about what actually went wrong.
  • Not using profiles (@Profile) to separate configuration for local, test, and production environments.
  • Skipping tests on service-layer logic because "it is just a thin wrapper," until it quietly stops being thin.

Common Mistakes

Avoid These Mistakes
  • Defaulting to field injection out of habit instead of constructor injection.
  • Treating a long list of @Autowired dependencies as normal instead of a signal to split the class.
  • Mixing package-by-layer and package-by-feature inconsistently within the same codebase.
  • Letting controllers reach directly into repositories, skipping the service layer entirely.
  • Copy-pasting configuration between environments instead of using Spring profiles.

Best Practices

  • Use constructor injection for required dependencies; reserve setter injection for genuinely optional ones.
  • Treat a large constructor parameter list as a prompt to split the class, not as something to hide with field injection.
  • Pick one package structure (by layer or by feature) and apply it consistently across the codebase.
  • Keep controllers thin, services focused on business logic, and repositories focused on data access.
  • Write unit tests for service-layer logic as it is written, not after it has grown complicated.

Frequently Asked Questions

It is sometimes used in test classes for brevity, but for production application code, constructor injection is the strongly preferred, widely recommended approach.

Not necessarily - for small applications, package-by-layer is simpler to navigate. Package-by-feature earns its complexity once an application has enough distinct features that layer folders become unwieldy.

There is no strict number, but once a constructor regularly needs five or more collaborators, it is worth asking whether the class has taken on more than one responsibility.

Key Takeaways

  • Constructor injection makes dependencies explicit, required, and testable; field injection hides them.
  • A bloated list of autowired dependencies is usually a sign a class is doing too much.
  • Package-by-layer suits smaller apps; package-by-feature scales better for larger ones.
  • Keep business logic in services, not controllers or repositories.
  • Use Spring profiles to separate configuration across environments instead of duplicating it.

Summary

These conventions are what separate a Spring application that stays maintainable from one that slowly turns into a tangle of hidden dependencies and mixed responsibilities. In the final lesson, you will apply everything from this course - controllers, services, JdbcTemplate repositories, validation, and clean structure - to plan and build one small real-world project end to end.

Next Lesson →

Real-World Project