Java Spring Boot
Best practices for Java Spring Boot development
# Java Spring Boot Development Best Practices
## 1. Project Structure & Configuration
Organize your Spring Boot application with proper package structure and configuration.
```java
// src/main/java/com/example/myapp/Application.java
package com.example.myapp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication
@EnableTransactionManagement
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// application.yml
server:
port: 8080
servlet:
context-path: /api
spring:
application:
name: my-app
datasource:
url: jdbc:postgresql://localhost:5432/myappdb
username: ${DB_USERNAME:admin}
password: ${DB_PASSWORD:password}
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: validate
show-sql: false
properties:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.PostgreSQLDialect
flyway:
enabled: true
locations: classpath:db/migration
logging:
level:
com.example.myapp: DEBUG
org.springframework.security: DEBUG
```
## 2. Entity & Repository Layer
Create JPA entities and repositories with proper relationships and queries.
```java
// src/main/java/com/example/myapp/entity/User.java
package com.example.myapp.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Table(name = "users", indexes = {
@Index(name = "idx_user_email", columnList = "email", unique = true)
})
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Name is required")
@Column(nullable = false, length = 100)
private String name;
@Email(message = "Email should be valid")
@NotBlank(message = "Email is required")
@Column(nullable = false, unique = true, length = 255)
private String email;
@Column(name = "is_active")
@Builder.Default
private Boolean active = true;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Post> posts;
}
// src/main/java/com/example/myapp/repository/UserRepository.java
package com.example.myapp.repository;
import com.example.myapp.entity.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
List<User> findByActiveTrue();
Page<User> findByActiveTrueOrderByCreatedAtDesc(Pageable pageable);
@Query("SELECT u FROM User u WHERE u.name LIKE %:name% AND u.active = true")
List<User> findActiveUsersByNameContaining(@Param("name") String name);
@Query(value = "SELECT * FROM users u WHERE u.created_at > NOW() - INTERVAL '30 days'",
nativeQuery = true)
List<User> findRecentUsers();
boolean existsByEmail(String email);
}
```
## 3. Service Layer with Business Logic
Implement business logic in service classes with proper transaction management.
```java
// src/main/java/com/example/myapp/service/UserService.java
package com.example.myapp.service;
import com.example.myapp.dto.CreateUserRequest;
import com.example.myapp.dto.UpdateUserRequest;
import com.example.myapp.dto.UserResponse;
import com.example.myapp.entity.User;
import com.example.myapp.exception.UserAlreadyExistsException;
import com.example.myapp.exception.UserNotFoundException;
import com.example.myapp.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional(readOnly = true)
public class UserService {
private final UserRepository userRepository;
private final UserMapper userMapper;
public Page<UserResponse> getAllUsers(Pageable pageable) {
log.debug("Fetching users with pagination: {}", pageable);
return userRepository.findByActiveTrueOrderByCreatedAtDesc(pageable)
.map(userMapper::toResponse);
}
public UserResponse getUserById(Long id) {
log.debug("Fetching user by id: {}", id);
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));
return userMapper.toResponse(user);
}
public UserResponse getUserByEmail(String email) {
log.debug("Fetching user by email: {}", email);
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UserNotFoundException("User not found with email: " + email));
return userMapper.toResponse(user);
}
@Transactional
public UserResponse createUser(CreateUserRequest request) {
log.debug("Creating new user with email: {}", request.getEmail());
if (userRepository.existsByEmail(request.getEmail())) {
throw new UserAlreadyExistsException("User already exists with email: " + request.getEmail());
}
User user = User.builder()
.name(request.getName())
.email(request.getEmail().toLowerCase())
.active(true)
.build();
User savedUser = userRepository.save(user);
log.info("Created user with id: {}", savedUser.getId());
return userMapper.toResponse(savedUser);
}
@Transactional
public UserResponse updateUser(Long id, UpdateUserRequest request) {
log.debug("Updating user with id: {}", id);
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));
if (request.getName() != null) {
user.setName(request.getName());
}
if (request.getEmail() != null && !request.getEmail().equals(user.getEmail())) {
if (userRepository.existsByEmail(request.getEmail())) {
throw new UserAlreadyExistsException("Email already in use: " + request.getEmail());
}
user.setEmail(request.getEmail().toLowerCase());
}
User updatedUser = userRepository.save(user);
log.info("Updated user with id: {}", updatedUser.getId());
return userMapper.toResponse(updatedUser);
}
@Transactional
public void deleteUser(Long id) {
log.debug("Deleting user with id: {}", id);
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));
user.setActive(false);
userRepository.save(user);
log.info("Soft deleted user with id: {}", id);
}
}
```
## 4. REST Controllers with Validation
Create RESTful controllers with proper validation and error handling.
```java
// src/main/java/com/example/myapp/controller/UserController.java
package com.example.myapp.controller;
import com.example.myapp.dto.CreateUserRequest;
import com.example.myapp.dto.UpdateUserRequest;
import com.example.myapp.dto.UserResponse;
import com.example.myapp.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/v1/users")
@RequiredArgsConstructor
@Tag(name = "Users", description = "User management operations")
public class UserController {
private final UserService userService;
@GetMapping
@Operation(summary = "Get all users", description = "Retrieve paginated list of active users")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Successfully retrieved users"),
@ApiResponse(responseCode = "400", description = "Invalid pagination parameters")
})
public ResponseEntity<Page<UserResponse>> getAllUsers(
@PageableDefault(size = 20, sort = "createdAt") Pageable pageable) {
Page<UserResponse> users = userService.getAllUsers(pageable);
return ResponseEntity.ok(users);
}
@GetMapping("/{id}")
@Operation(summary = "Get user by ID", description = "Retrieve a user by their unique identifier")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "User found"),
@ApiResponse(responseCode = "404", description = "User not found")
})
public ResponseEntity<UserResponse> getUserById(
@Parameter(description = "User ID") @PathVariable Long id) {
UserResponse user = userService.getUserById(id);
return ResponseEntity.ok(user);
}
@PostMapping
@Operation(summary = "Create new user", description = "Create a new user account")
@ApiResponses({
@ApiResponse(responseCode = "201", description = "User created successfully"),
@ApiResponse(responseCode = "400", description = "Invalid user data"),
@ApiResponse(responseCode = "409", description = "User already exists")
})
public ResponseEntity<UserResponse> createUser(@Valid @RequestBody CreateUserRequest request) {
UserResponse user = userService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED).body(user);
}
@PutMapping("/{id}")
@Operation(summary = "Update user", description = "Update an existing user")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "User updated successfully"),
@ApiResponse(responseCode = "400", description = "Invalid user data"),
@ApiResponse(responseCode = "404", description = "User not found"),
@ApiResponse(responseCode = "409", description = "Email already in use")
})
public ResponseEntity<UserResponse> updateUser(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
UserResponse user = userService.updateUser(id, request);
return ResponseEntity.ok(user);
}
@DeleteMapping("/{id}")
@Operation(summary = "Delete user", description = "Soft delete a user account")
@ApiResponses({
@ApiResponse(responseCode = "204", description = "User deleted successfully"),
@ApiResponse(responseCode = "404", description = "User not found")
})
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
```
## 5. DTOs and Validation
Define data transfer objects with comprehensive validation.
```java
// src/main/java/com/example/myapp/dto/CreateUserRequest.java
package com.example.myapp.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Data
@Schema(description = "Request to create a new user")
public class CreateUserRequest {
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters")
@Schema(description = "User's full name", example = "John Doe")
private String name;
@NotBlank(message = "Email is required")
@Email(message = "Email should be valid")
@Size(max = 255, message = "Email must not exceed 255 characters")
@Schema(description = "User's email address", example = "john.doe@example.com")
private String email;
}
// src/main/java/com/example/myapp/dto/UpdateUserRequest.java
package com.example.myapp.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Data
@Schema(description = "Request to update user information")
public class UpdateUserRequest {
@Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters")
@Schema(description = "User's full name", example = "Jane Doe")
private String name;
@Email(message = "Email should be valid")
@Size(max = 255, message = "Email must not exceed 255 characters")
@Schema(description = "User's email address", example = "jane.doe@example.com")
private String email;
}
// src/main/java/com/example/myapp/dto/UserResponse.java
package com.example.myapp.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Builder
@Schema(description = "User information response")
public class UserResponse {
@Schema(description = "User's unique identifier", example = "1")
private Long id;
@Schema(description = "User's full name", example = "John Doe")
private String name;
@Schema(description = "User's email address", example = "john.doe@example.com")
private String email;
@Schema(description = "Whether the user account is active", example = "true")
private Boolean active;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
@Schema(description = "When the user account was created", example = "2024-01-01T10:00:00")
private LocalDateTime createdAt;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
@Schema(description = "When the user account was last updated", example = "2024-01-01T10:00:00")
private LocalDateTime updatedAt;
}
```
## 6. Exception Handling
Implement comprehensive exception handling with proper error responses.
```java
// src/main/java/com/example/myapp/exception/GlobalExceptionHandler.java
package com.example.myapp.exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleUserNotFoundException(
UserNotFoundException ex, WebRequest request) {
log.error("User not found: {}", ex.getMessage());
ErrorResponse error = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.NOT_FOUND.value())
.error("Not Found")
.message(ex.getMessage())
.path(request.getDescription(false).replace("uri=", ""))
.build();
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(UserAlreadyExistsException.class)
public ResponseEntity<ErrorResponse> handleUserAlreadyExistsException(
UserAlreadyExistsException ex, WebRequest request) {
log.error("User already exists: {}", ex.getMessage());
ErrorResponse error = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.CONFLICT.value())
.error("Conflict")
.message(ex.getMessage())
.path(request.getDescription(false).replace("uri=", ""))
.build();
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ValidationErrorResponse> handleValidationException(
MethodArgumentNotValidException ex, WebRequest request) {
log.error("Validation error: {}", ex.getMessage());
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach(error -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
ValidationErrorResponse error = ValidationErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.BAD_REQUEST.value())
.error("Validation Failed")
.message("Input validation failed")
.path(request.getDescription(false).replace("uri=", ""))
.validationErrors(errors)
.build();
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(
Exception ex, WebRequest request) {
log.error("Unexpected error occurred", ex);
ErrorResponse error = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.INTERNAL_SERVER_ERROR.value())
.error("Internal Server Error")
.message("An unexpected error occurred")
.path(request.getDescription(false).replace("uri=", ""))
.build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
```
## 7. Configuration & Security
Configure security and application properties properly.
```java
// src/main/java/com/example/myapp/config/SecurityConfig.java
package com.example.myapp.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authz -> authz
.requestMatchers("/actuator/**", "/v3/api-docs/**", "/swagger-ui/**").permitAll()
.requestMatchers("/api/v1/auth/**").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
}
// src/main/java/com/example/myapp/config/OpenApiConfig.java
package com.example.myapp.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("My App API")
.version("1.0.0")
.description("REST API for My App")
.license(new License()
.name("MIT")
.url("https://opensource.org/licenses/MIT")));
}
}
```
## 8. Testing with JUnit 5
Write comprehensive tests for all application layers.
```java
// src/test/java/com/example/myapp/service/UserServiceTest.java
package com.example.myapp.service;
import com.example.myapp.dto.CreateUserRequest;
import com.example.myapp.dto.UserResponse;
import com.example.myapp.entity.User;
import com.example.myapp.exception.UserAlreadyExistsException;
import com.example.myapp.exception.UserNotFoundException;
import com.example.myapp.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private UserMapper userMapper;
@InjectMocks
private UserService userService;
private User testUser;
private CreateUserRequest createRequest;
private UserResponse userResponse;
@BeforeEach
void setUp() {
testUser = User.builder()
.id(1L)
.name("John Doe")
.email("john.doe@example.com")
.active(true)
.build();
createRequest = new CreateUserRequest();
createRequest.setName("John Doe");
createRequest.setEmail("john.doe@example.com");
userResponse = UserResponse.builder()
.id(1L)
.name("John Doe")
.email("john.doe@example.com")
.active(true)
.build();
}
@Test
void getUserById_WhenUserExists_ShouldReturnUser() {
// Given
when(userRepository.findById(1L)).thenReturn(Optional.of(testUser));
when(userMapper.toResponse(testUser)).thenReturn(userResponse);
// When
UserResponse result = userService.getUserById(1L);
// Then
assertThat(result).isNotNull();
assertThat(result.getId()).isEqualTo(1L);
assertThat(result.getName()).isEqualTo("John Doe");
assertThat(result.getEmail()).isEqualTo("john.doe@example.com");
verify(userRepository).findById(1L);
verify(userMapper).toResponse(testUser);
}
@Test
void getUserById_WhenUserNotExists_ShouldThrowException() {
// Given
when(userRepository.findById(1L)).thenReturn(Optional.empty());
// When & Then
assertThatThrownBy(() -> userService.getUserById(1L))
.isInstanceOf(UserNotFoundException.class)
.hasMessage("User not found with id: 1");
verify(userRepository).findById(1L);
verifyNoInteractions(userMapper);
}
@Test
void createUser_WhenEmailNotExists_ShouldCreateUser() {
// Given
when(userRepository.existsByEmail("john.doe@example.com")).thenReturn(false);
when(userRepository.save(any(User.class))).thenReturn(testUser);
when(userMapper.toResponse(testUser)).thenReturn(userResponse);
// When
UserResponse result = userService.createUser(createRequest);
// Then
assertThat(result).isNotNull();
assertThat(result.getName()).isEqualTo("John Doe");
assertThat(result.getEmail()).isEqualTo("john.doe@example.com");
verify(userRepository).existsByEmail("john.doe@example.com");
verify(userRepository).save(any(User.class));
verify(userMapper).toResponse(testUser);
}
@Test
void createUser_WhenEmailExists_ShouldThrowException() {
// Given
when(userRepository.existsByEmail("john.doe@example.com")).thenReturn(true);
// When & Then
assertThatThrownBy(() -> userService.createUser(createRequest))
.isInstanceOf(UserAlreadyExistsException.class)
.hasMessage("User already exists with email: john.doe@example.com");
verify(userRepository).existsByEmail("john.doe@example.com");
verify(userRepository, never()).save(any(User.class));
verifyNoInteractions(userMapper);
}
}
// Integration test
@SpringBootTest
@TestPropertySource(locations = "classpath:application-test.properties")
@Transactional
class UserControllerIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private UserRepository userRepository;
@Test
void createUser_ShouldReturnCreatedUser() {
// Given
CreateUserRequest request = new CreateUserRequest();
request.setName("Jane Doe");
request.setEmail("jane.doe@example.com");
// When
ResponseEntity<UserResponse> response = restTemplate.postForEntity(
"/api/v1/users", request, UserResponse.class);
// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getName()).isEqualTo("Jane Doe");
assertThat(response.getBody().getEmail()).isEqualTo("jane.doe@example.com");
// Verify database
Optional<User> savedUser = userRepository.findByEmail("jane.doe@example.com");
assertThat(savedUser).isPresent();
}
}
```
## 9. Database Migration with Flyway
Manage database schema changes with Flyway migrations.
```sql
-- src/main/resources/db/migration/V1__Create_users_table.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_user_email ON users(email);
CREATE INDEX idx_user_active_created ON users(is_active, created_at);
-- src/main/resources/db/migration/V2__Create_posts_table.sql
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT,
user_id BIGINT NOT NULL,
published BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX idx_post_user_id ON posts(user_id);
CREATE INDEX idx_post_published_created ON posts(published, created_at);
```
## 10. Caching & Performance
Implement caching strategies for improved performance.
```java
// src/main/java/com/example/myapp/config/CacheConfig.java
package com.example.myapp.config;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("users", "user-emails");
}
}
// Service with caching
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional(readOnly = true)
public class CachedUserService {
private final UserRepository userRepository;
@Cacheable(value = "users", key = "#id")
public UserResponse getUserById(Long id) {
log.debug("Fetching user from database: {}", id);
// Implementation
}
@Cacheable(value = "user-emails", key = "#email")
public UserResponse getUserByEmail(String email) {
log.debug("Fetching user by email from database: {}", email);
// Implementation
}
@CacheEvict(value = {"users", "user-emails"}, key = "#result.id")
@Transactional
public UserResponse updateUser(Long id, UpdateUserRequest request) {
log.debug("Updating and evicting cache for user: {}", id);
// Implementation
}
@CacheEvict(value = {"users", "user-emails"}, allEntries = true)
@Transactional
public void clearAllCaches() {
log.info("Clearing all user caches");
}
}
```
## Checklist
- [ ] Use proper Spring Boot project structure with clear package organization
- [ ] Create JPA entities with appropriate annotations and relationships
- [ ] Implement repositories with custom queries and proper naming conventions
- [ ] Build service layer with business logic and transaction management
- [ ] Create REST controllers with proper validation and error handling
- [ ] Define DTOs with comprehensive validation annotations
- [ ] Implement global exception handling with meaningful error responses
- [ ] Configure security and application properties appropriately
- [ ] Write comprehensive unit and integration tests
- [ ] Use Flyway for database schema migration management
- [ ] Implement caching strategies for performance optimization
- [ ] Follow Java naming conventions and Spring Boot best practices
- [ ] Use Lombok to reduce boilerplate code
- [ ] Implement proper logging throughout the application
- [ ] Document APIs with OpenAPI/Swagger annotations