5 Common Spring Boot Anti‑Patterns You Must Avoid

The article examines five high‑frequency Spring Boot anti‑patterns—field injection, returning entities from controllers, overusing @Transactional, generic exception handling, and embedding business logic in controllers—showing problematic code snippets, real‑world consequences, and concise refactored examples to improve testability, performance, and maintainability.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
5 Common Spring Boot Anti‑Patterns You Must Avoid

Environment: Spring Boot 3.5.0

1. Introduction

A recent code review of a Spring Boot project revealed five fatal hidden risks that can cause performance bottlenecks, obscure bugs, and difficult troubleshooting once the application is deployed to production. The article uses concrete examples to demonstrate why these patterns are harmful and provides practical refactorings.

2. Real‑world cases

2.1 Field injection with @Autowired

Field injection looks concise but hides class dependencies and makes unit testing hard because the injected bean cannot be easily mocked.

@Service
public class OrderService {
  @Autowired
  private OrderRepository orderRepository;

  public Order findById(Long id) {
    return orderRepository.findById(id).orElseThrow();
  }
}

Spring injects the dependency, but the test cannot replace it with a mock.

@Service
public class OrderService {
  private final OrderRepository orderRepository;

  public OrderService(OrderRepository orderRepository) {
    this.orderRepository = orderRepository;
  }

  public Order findById(Long id) {
    return orderRepository.findById(id).orElseThrow();
  }
}

The constructor makes the dependency explicit, allowing a mock to be passed in tests, saving hours of test‑writing effort.

2.2 Returning JPA entities from controller endpoints

Directly returning entities exposes internal structure, can trigger lazy‑loading errors, and prevents control over the response fields.

@RestController
public class UserController {
  private final UserRepository userRepository;

  @GetMapping("/users/{id}")
  public User getUser(@PathVariable Long id) {
    return userRepository.findById(id).orElseThrow();
  }
}

This approach caused an API crash in production due to lazy loading.

public record UserResponse(Long id, String name, String email) {}

@RestController
public class UserController {
  private final UserRepository userRepository;

  @GetMapping("/users/{id}")
  public UserResponse getUser(@PathVariable Long id) {
    User user = userRepository.findById(id).orElseThrow();
    return new UserResponse(user.getId(), user.getName(), user.getEmail());
  }
}

The DTO isolates the API contract and avoids lazy‑loading pitfalls.

2.3 Overusing @Transactional

Placing @Transactional on many methods keeps transactions open longer than necessary, slowing the system and hiding bugs. An example from a billing service showed a method that locked the database for an excessive period, causing downstream latency.

@Service
public class PaymentService {
  @Transactional
  public void processPayment(Long orderId) {
    // long logic
    // external API calls
  }
}

The transaction remains open throughout the whole method.

@Service
public class PaymentService {
  private final PaymentRepository paymentRepository;
  private final PaymentService paymentService;

  @Transactional
  public void savePayment(Payment payment) {
    paymentRepository.save(payment);
  }

  public void processPayment(Long orderId) {
    // work outside the transaction
    Payment payment = new Payment(orderId);
    // avoid this.self call, otherwise transaction is lost
    paymentService.savePayment(payment);
  }
}

Keeping the transactional boundary short reduces response time and improves throughput.

2.4 Generic exception handling

Catching broad Exception masks the real problem and makes debugging difficult. The author spent two days tracing a bug hidden by such a catch.

public void createOrder(Order order) {
  try {
    orderRepository.save(order);
  } catch (Exception e) {
    throw new RuntimeException("Something went wrong");
  }
}

This loses the original cause.

public void createOrder(Order order) {
  try {
    orderRepository.save(order);
  } catch (DataAccessException e) {
    throw new OrderSaveException("Failed to save order", e);
  }
}

A global handler can then translate the custom exception into a proper HTTP response:

@RestControllerAdvice
public class GlobalExceptionHandler {
  @ExceptionHandler(OrderSaveException.class)
  public ResponseEntity<String> handle(OrderSaveException ex) {
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                         .body(ex.getMessage());
  }
}

2.5 Embedding business logic in controllers

Controllers should be thin. Mixing business logic makes the code hard to test and reuse. An example controller contained a 200‑line conditional block.

@RestController
@RequestMapping("/orders")
public class OrderController {
  private final OrderRepository orderRepository;

  @PostMapping
  public String createOrder(@Validated @RequestBody Order order) {
    if (order.getAmount() > 1000) {
      // business logic
    }
    orderRepository.save(order);
    return "success";
  }
}

Refactoring moves the logic to a service layer:

@RestController
@RequestMapping("/orders")
public class OrderController {
  private final OrderService orderService;

  @PostMapping
  public String createOrder(@Validated @RequestBody Order order) {
    orderService.create(order);
    return "created";
  }
}

@Service
public class OrderService {
  public void create(Order order) {
    if (order.getAmount() > 1000) {
      // business logic
    }
    // save logic
  }
}

The controller becomes concise, the logic is reusable and testable.

3. Conclusion

By avoiding field injection, returning DTOs instead of entities, limiting the scope of @Transactional, handling specific exceptions, and keeping controllers thin, developers can write Spring Boot applications that are easier to test, more performant, and less prone to production failures.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Exception HandlingSpring BootDependency InjectionTransaction ManagementAnti‑PatternController Design
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.