Here is the complete set that covers what teams actually use in real Spring Boot code (design, Spring patterns, testing, security, ops).
1) Prefer constructor injection (not field injection)
Principle: Dependencies should be explicit and testable.
✅ Good
@Service
class OrderService {
private final OrderRepository repo;
OrderService(OrderRepository repo) {
this.repo = repo;
}
}
❌ Bad
@Service
class OrderService {
@Autowired
private OrderRepository repo; // hidden dependency
}
2) Keep controllers thin, put logic in services
Principle: Controllers handle HTTP, services handle business rules.
✅ Good
@RestController
class OrderController {
private final OrderService service;
OrderController(OrderService service) { this.service = service; }
@PostMapping("/orders")
OrderResponse create(@RequestBody CreateOrderRequest req) {
return service.create(req);
}
}
❌ Bad
@RestController
class OrderController {
@Autowired OrderRepository repo;
@PostMapping("/orders")
Order save(@RequestBody Order o) {
o.setStatus("NEW");
o.setCreatedAt(Instant.now());
return repo.save(o); // business logic in controller
}
}
3) Separate API DTOs from JPA entities
Principle: Entities are persistence models, not public API contracts.
✅ Good
record CreateUserRequest(String email, String name) {}
record UserResponse(Long id, String email, String name) {}
❌ Bad
@PostMapping("/users")
public UserEntity create(@RequestBody UserEntity entity) { // leaking entity
return repo.save(entity);
}
4) Validate inputs with Bean Validation
Principle: Reject bad input early and consistently.
✅ Good
record SignupRequest(
@jakarta.validation.constraints.Email String email,
@jakarta.validation.constraints.NotBlank String password
) {}
@PostMapping("/signup")
UserResponse signup(@Valid @RequestBody SignupRequest req) { ... }
❌ Bad
@PostMapping("/signup")
UserResponse signup(@RequestBody Map<String, String> req) {
if (!req.get("email").contains("@")) throw new RuntimeException("bad");
...
}
5) Centralize error handling with @ControllerAdvice
Principle: One place for consistent HTTP errors.
✅ Good
@RestControllerAdvice
class ApiErrors {
@ExceptionHandler(NotFoundException.class)
ResponseEntity<?> handle(NotFoundException e) {
return ResponseEntity.status(404).body(Map.of("error", e.getMessage()));
}
}
❌ Bad
@GetMapping("/orders/{id}")
Order get(@PathVariable Long id) {
try { return service.get(id); }
catch (Exception e) { throw new ResponseStatusException(HttpStatus.NOT_FOUND); }
}
6) Use correct HTTP status codes and responses
Principle: API should be predictable.
✅ Good
@PostMapping("/orders")
ResponseEntity<OrderResponse> create(@Valid @RequestBody CreateOrderRequest req) {
var created = service.create(req);
return ResponseEntity.status(201).body(created);
}
❌ Bad
@PostMapping("/orders")
OrderResponse create(@RequestBody CreateOrderRequest req) { // always 200
return service.create(req);
}
7) Use meaningful package structure (by feature is often best)
Principle: Code should be easy to find.
✅ Good
com.app.orders/
OrderController, OrderService, OrderRepository, dto/
❌ Bad
com.app.controller/, com.app.service/, com.app.repo/
(100+ files per folder)
8) Keep transaction boundaries in service layer
Principle: Transactions should wrap business operations, not controllers.
✅ Good
@Service
class TransferService {
@Transactional
void transfer(long from, long to, BigDecimal amount) { ... }
}
❌ Bad
@RestController
class TransferController {
@Transactional
@PostMapping("/transfer")
void transfer(...) { ... } // transaction tied to web layer
}
9) Don’t swallow exceptions; wrap with context
Principle: Errors should keep meaning and traceability.
✅ Good
try {
client.call();
} catch (IOException e) {
throw new ExternalCallException("payment-provider failed", e);
}
❌ Bad
try {
client.call();
} catch (Exception e) {
return null; // silent failure
}
10) Use logging wisely (no System.out)
Principle: Use structured logs, correct levels.
✅ Good
private static final Logger log = LoggerFactory.getLogger(getClass());
log.info("Created order id={}", orderId);
❌ Bad
System.out.println("Created order " + orderId);
11) Never log secrets (tokens, passwords, full cards)
Principle: Logs are not a safe place.
✅ Good
log.info("Login attempt email={}", email);
❌ Bad
log.info("Login payload={}", req); // might contain password
12) Use configuration properties instead of sprinkling @Value
Principle: Strong typing + one place for config.
✅ Good
@ConfigurationProperties(prefix="app.storage")
record StorageProps(String bucket, int timeoutSeconds) {}
@Bean
StorageClient client(StorageProps p) { ... }
❌ Bad
@Value("${app.storage.bucket}") String bucket;
@Value("${app.storage.timeoutSeconds}") int timeout; // scattered
13) Use profiles for environment differences
Principle: No “if prod do X” in code.
✅ Good
@Profile("dev")
@Bean DataSource devDs() { ... }
@Profile("prod")
@Bean DataSource prodDs() { ... }
❌ Bad
if ("prod".equals(System.getenv("ENV"))) { ... } // brittle
14) Prefer immutability for DTOs and configs
Principle: Fewer bugs, easier reasoning.
✅ Good
record Money(BigDecimal amount, String currency) {}
❌ Bad
class Money {
public BigDecimal amount;
public String currency; // mutable and public
}
15) Avoid “God services”; keep single responsibility
Principle: One class should do one job.
✅ Good
class PricingService { ... }
class InventoryService { ... }
class CheckoutService { ... } // composes them
❌ Bad
class AppService { // does pricing + inventory + email + auth + reports
}
16) Use repository methods that match intent
Principle: Queries should be readable.
✅ Good
interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
❌ Bad
List<User> users = repo.findAll();
return users.stream().filter(u -> u.getEmail().equals(email)).findFirst();
17) Avoid N+1 queries, fetch intentionally
Principle: Performance should not be accidental.
✅ Good
@Query("select o from Order o join fetch o.items where o.id = :id")
Optional<Order> findWithItems(@Param("id") Long id);
❌ Bad
Order o = repo.findById(id).orElseThrow();
// later: o.getItems().size(); // triggers lazy loads per item chain
18) Use pagination for list endpoints
Principle: Protect DB and API.
✅ Good
@GetMapping("/users")
Page<UserResponse> list(Pageable pageable) { ... }
❌ Bad
@GetMapping("/users")
List<UserResponse> list() { return service.getAllUsers(); } // can explode
19) Make endpoints idempotent where it matters
Principle: Retries should be safe.
✅ Good
@PutMapping("/users/{id}")
UserResponse update(@PathVariable Long id, @RequestBody UpdateUserRequest req) { ... }
❌ Bad
@PostMapping("/users/{id}/update") // action-style, unclear semantics
20) Prefer explicit response shapes (don’t return Map everywhere)
Principle: Strong contracts help clients.
✅ Good
record HealthResponse(String status, Instant time) {}
❌ Bad
return Map.of("status", "ok", "t", System.currentTimeMillis());
21) Use @Transactional(readOnly = true) for read paths
Principle: Helps performance and intent.
✅ Good
@Transactional(readOnly = true)
UserResponse get(long id) { ... }
❌ Bad
@Transactional
UserResponse get(long id) { ... } // writes not needed
22) Do not put external calls inside DB transactions
Principle: Transactions should be short.
✅ Good
var paymentId = paymentClient.charge(req);
@Transactional
void saveOrder(...) { ... }
❌ Bad
@Transactional
void checkout(...) {
paymentClient.charge(req); // slow network inside transaction
repo.save(order);
}
23) Use time abstractions for testability (Clock)
Principle: Don’t hardcode “now”.
✅ Good
@Service
class TokenService {
private final Clock clock;
TokenService(Clock clock) { this.clock = clock; }
Instant now() { return Instant.now(clock); }
}
❌ Bad
Instant issuedAt = Instant.now(); // hard to test
24) Avoid static state and singletons
Principle: Spring already manages lifecycles.
✅ Good
@Component
class IdGenerator { String next() { ... } }
❌ Bad
class IdGenerator {
static final IdGenerator INSTANCE = new IdGenerator();
}
25) Prefer interfaces at boundaries, not everywhere
Principle: Use interfaces where you have multiple impls or mocking needs.
✅ Good
interface EmailSender { void send(...); }
@Service class SesEmailSender implements EmailSender { ... }
❌ Bad
interface UserService { ... } // only one impl forever, adds noise
class UserServiceImpl implements UserService { ... }
26) Keep mapping logic in one place (mapper methods/classes)
Principle: Avoid scattered conversion code.
✅ Good
class UserMapper {
static UserResponse toResponse(User u) { return new UserResponse(u.getId(), u.getEmail(), u.getName()); }
}
❌ Bad
// mapping repeated in 7 controllers with slight differences
27) Use OpenAPI/Swagger annotations (or docs) for public APIs
Principle: Docs should not be tribal knowledge.
✅ Good
@Operation(summary = "Create order")
@PostMapping("/orders")
...
❌ Bad
// no docs, clients guess fields from examples
28) Write tests at multiple levels (unit + slice + integration)
Principle: Catch bugs early and reliably.
✅ Good
@WebMvcTest(OrderController.class)
class OrderControllerTest { ... }
❌ Bad
// only manual testing, no automated tests
29) Use Testcontainers (or real dependencies) for DB integration tests
Principle: H2-only tests can lie.
✅ Good
@SpringBootTest
@Testcontainers
class OrderRepoIT { ... }
❌ Bad
@SpringBootTest
class RepoTestUsingH2Only { ... } // passes but fails on Postgres
30) Secure endpoints with Spring Security, not custom “if role”
Principle: Central policy, less risk.
✅ Good
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/users/{id}")
void delete(@PathVariable long id) { ... }
❌ Bad
if (!currentUser.isAdmin()) throw new RuntimeException("no"); // easy to miss
31) Don’t expose internal exception messages directly to clients
Principle: Avoid leaking system details.
✅ Good
return ResponseEntity.status(500).body(Map.of("error", "Internal error"));
❌ Bad
return ResponseEntity.status(500).body(Map.of("error", e.toString()));
32) Use correlation IDs (trace IDs) for request tracking
Principle: Debugging needs linking logs across services.
✅ Good
// log pattern includes %X{traceId}; set it via filter once
❌ Bad
// no request id; logs can’t be tied to a single call
33) Use graceful shutdown and timeouts for clients
Principle: Prevent stuck threads and broken deploys.
✅ Good
// WebClient with timeouts; server.tomcat.threads tuned; graceful shutdown enabled
❌ Bad
new RestTemplate(); // defaults, no timeouts, can hang
34) Keep database schema changes managed (Flyway/Liquibase)
Principle: No “run this SQL manually” steps.
✅ Good
-- V3__add_status_to_orders.sql
ALTER TABLE orders ADD COLUMN status varchar(20) NOT NULL;
❌ Bad
// app fails in prod because a column is missing and nobody ran the SQL
35) Make code readable: names, small methods, no clever tricks
Principle: Maintainability wins.
✅ Good
boolean isEligible(User u) {
return u.isActive() && u.getAge() >= 18;
}
❌ Bad
boolean x(User u){return u.a()&&u.b()>17;} // cryptic

Top comments (0)