Introduction
You just finished writing a feature in your Spring Boot project. It works on your machine, tests are green — but is it really production-ready?
That’s where code reviews come in. A structured checklist helps catch hidden pitfalls, enforce best practices, and keep your application secure, performant, and maintainable.
This post shares a practical code review checklist for Spring Boot + Spring Data JPA projects, along with good/bad code examples.
**
- 1. Project Structure & Configuration ** Packages follow a clear domain- or feature-driven structure.
Environment configs (DB credentials, API keys) are externalized — not hardcoded.
Profiles (dev, test, prod) are properly used.
No unused dependencies in pom.xml / build.gradle.
**
- 2. Entity Design & JPA Mapping ** Entities use @Entity, @Table, and follow consistent naming.
Primary keys use @id with appropriate @GeneratedValue strategy.
Relationships use lazy loading by default (fetch = FetchType.LAZY).
equals() and hashCode() don’t depend on auto-generated IDs.
Bidirectional relationships are avoided unless necessary.
Prefer Set over List when duplicates aren’t allowed.
Entities are lightweight — no business logic inside.
Example (Bad vs. Good JPA Mapping):
❌ Bad
@Entity
public class Order {
@OneToMany(mappedBy = "order", fetch = FetchType.EAGER) // loads all items upfront
private List items;
}
✅ Good
@Entity
public class Order {
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY) // load only when needed
private Set items = new HashSet<>();
}
**
- 3. Repository Layer ** Repositories extend JpaRepository or CrudRepository.
Complex queries use @Query or Specification API (not inline JPQL).
Pagination (Pageable) is used for large datasets.
Custom repository implementations are separated.
No excessive use of findAll() that may cause performance issues.
Example (Good Repository Method with Pagination):
Page findByStatus(String status, Pageable pageable);
**
- 4. Service Layer & Transactions ** Business logic lives in services, not controllers or repositories.
@Transactional is applied at the service layer, not repository level.
Queries that don’t modify data use @Transactional(readOnly = true).
Services are stateless (no mutable shared fields).
Example (Transactional Service Method):
@Service
public class CustomerService {
@Transactional(readOnly = true)
public Customer getCustomer(Long id) {
return customerRepository.findById(id)
.orElseThrow(() -> new CustomerNotFoundException(id));
}
}
**
- 5. Controller Layer (REST APIs) ** REST endpoints follow naming conventions (/api/v1/customers/{id}).
Entities are not exposed directly — use DTOs instead.
Input validation with @valid and Bean Validation annotations.
Consistent response format (e.g., a standard wrapper object).
Correct HTTP status codes are returned (200, 201, 400, 404, etc.).
Exceptions handled centrally with @ControllerAdvice.
Example (Bad vs. Good Controller):
❌ Bad
@RestController
@RequestMapping("/customers")
public class CustomerController {
@PostMapping
public Customer create(@RequestBody Customer customer) {
return customerRepository.save(customer); // exposes entity directly
}
}
✅ Good
@RestController
@RequestMapping("/api/v1/customers")
public class CustomerController {
@PostMapping
public ResponseEntity<CustomerDto> create(@Valid @RequestBody CustomerDto dto) {
Customer saved = customerService.create(dto);
return ResponseEntity.status(HttpStatus.CREATED).body(new CustomerDto(saved));
}
}
**
- 6. Performance & Query Optimization ** Use projections (DTOs/interfaces) when full entity fetch isn’t needed.
Avoid N+1 select issues — check fetch strategies.
Entities and queries align with proper database indexes.
Frequently accessed data is cached (Spring Cache, Redis).
Batch operations used where applicable.
Example (DTO Projection in Repository):
public interface CustomerSummary {
String getName();
String getEmail();
}
List findByActiveTrue();
**
- 7. Security ** No sensitive data logged or exposed in responses.
Spring Security (or equivalent) properly configured.
Method-level security (@PreAuthorize) applied where needed.
CORS and CSRF configured correctly.
Queries are parameterized — no string concatenation.
Example (Method Security with PreAuthorize):
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
**
- 8. Testing ** Unit tests cover services and controllers with meaningful assertions.
Repository tests use @DataJpaTest.
Integration tests rely on test containers or H2 with aligned schema.
External dependencies mocked (@MockBean, WireMock).
Transaction rollback scenarios tested.
Example (Repository Test):
@DataJpaTest
class CustomerRepositoryTest {
@Autowired
private CustomerRepository repository;
@Test
void shouldFindByEmail() {
Customer customer = repository.save(new Customer("John", "john@mail.com"));
Optional<Customer> found = repository.findByEmail("john@mail.com");
assertThat(found).isPresent();
}
}
**
- 9. Logging & Monitoring ** Use SLF4J (log.info, log.error) instead of System.out.println.
Avoid logging sensitive information (credentials, tokens).
Add correlation IDs (MDC) for tracing requests.
Health checks (/actuator/health) are enabled for monitoring.
Metrics and tracing are integrated (Micrometer, Prometheus, Zipkin, etc.).
**
- 10. General Code Quality ** Code follows naming conventions and readability standards.
Magic numbers and strings are avoided (use constants/enums).
Null-safety is considered (Optional, @NonNull).
Proper exception hierarchy is maintained (CustomBusinessException, etc.).
Dead code, commented-out blocks, and unused imports are removed.
🎯 Final Thoughts
This checklist isn’t meant to replace in-depth reviews, but it provides a structured guide to ensure Spring Boot + JPA projects remain clean, efficient, and secure.
Whether you’re a reviewer or developer, use this as a baseline — and adapt it to your team’s coding standards. Over time, a consistent checklist can significantly improve software quality and reduce technical debt.
Top comments (0)