Complete development reference
Development// Spring Initializr: https://start.spring.io
// Common dependencies:
// - Spring Web
// - Spring Data JPA
// - Spring Security
// - Spring Validation
// - PostgreSQL / H2 Driver
// - Lombok
// Project structure
src/main/java/com/example/demo/
โโโ DemoApplication.java // @SpringBootApplication
โโโ controller/
โ โโโ UserController.java
โโโ service/
โ โโโ UserService.java
โโโ repository/
โ โโโ UserRepository.java
โโโ model/
โ โโโ User.java
โโโ dto/
โ โโโ CreateUserRequest.java
โ โโโ UserResponse.java
โโโ exception/
โ โโโ GlobalExceptionHandler.java
โโโ config/
โโโ SecurityConfig.java
src/main/resources/
โโโ application.yml
โโโ application-dev.yml<!-- Spring Boot Parent -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>| Annotation | Purpose |
|---|---|
@SpringBootApplication | Main class โ combines @Configuration + @EnableAutoConfiguration + @ComponentScan |
@RestController | REST controller โ @Controller + @ResponseBody |
@Service | Business logic layer bean |
@Repository | Data access layer bean (adds exception translation) |
@Component | Generic Spring-managed bean |
@Configuration | Java-based configuration class |
@Bean | Method-level โ registers return value as a bean |
@Autowired | Dependency injection (prefer constructor injection) |
@Value("${key}") | Inject property value |
@Qualifier("name") | Disambiguate beans of the same type |
@Primary | Default bean when multiple candidates exist |
@Profile("dev") | Activate bean only in specific profile |
@ConditionalOnProperty | Bean creation based on configuration property |
@Transactional | Transaction management on method/class |
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor // Lombok โ constructor injection
public class UserController {
private final UserService userService;
// GET /api/v1/users
@GetMapping
public List<UserResponse> getAll() {
return userService.findAll();
}
// GET /api/v1/users/{id}
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getById(@PathVariable Long id) {
return ResponseEntity.ok(userService.findById(id));
}
// GET /api/v1/users?status=ACTIVE&page=0&size=10
@GetMapping
public Page<UserResponse> search(
@RequestParam(defaultValue = "ACTIVE") String status,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size
) {
return userService.search(status, PageRequest.of(page, size));
}
// POST /api/v1/users
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public UserResponse create(@Valid @RequestBody CreateUserRequest req) {
return userService.create(req);
}
// PUT /api/v1/users/{id}
@PutMapping("/{id}")
public UserResponse update(@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest req) {
return userService.update(id, req);
}
// DELETE /api/v1/users/{id}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
userService.delete(id);
}
}@Entity
@Table(name = "users")
@Data // Lombok: getters, setters, toString, equals, hashCode
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String name;
@Column(unique = true, nullable = false)
private String email;
@Enumerated(EnumType.STRING)
private Status status = Status.ACTIVE;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Post> posts = new ArrayList<>();
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
private Department department;
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Derived query methods
Optional<User> findByEmail(String email);
List<User> findByStatus(Status status);
List<User> findByNameContainingIgnoreCase(String name);
boolean existsByEmail(String email);
long countByStatus(Status status);
// @Query (JPQL)
@Query("SELECT u FROM User u WHERE u.status = :status ORDER BY u.createdAt DESC")
Page<User> findActiveUsers(@Param("status") Status status, Pageable pageable);
// Native query
@Query(value = "SELECT * FROM users WHERE email LIKE %:domain", nativeQuery = true)
List<User> findByEmailDomain(@Param("domain") String domain);
// Modifying query
@Modifying
@Query("UPDATE User u SET u.status = :status WHERE u.id = :id")
int updateStatus(@Param("id") Long id, @Param("status") Status status);
}
// JpaRepository provides:
// save(), saveAll(), findById(), findAll(), findAll(Pageable),
// deleteById(), delete(), count(), existsById()@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserService {
private final UserRepository userRepo;
private final UserMapper mapper;
public List<UserResponse> findAll() {
return userRepo.findAll()
.stream()
.map(mapper::toResponse)
.toList();
}
public UserResponse findById(Long id) {
return userRepo.findById(id)
.map(mapper::toResponse)
.orElseThrow(() -> new ResourceNotFoundException("User", id));
}
@Transactional
public UserResponse create(CreateUserRequest req) {
if (userRepo.existsByEmail(req.email())) {
throw new DuplicateException("Email already exists");
}
User user = mapper.toEntity(req);
return mapper.toResponse(userRepo.save(user));
}
@Transactional
public void delete(Long id) {
if (!userRepo.existsById(id)) {
throw new ResourceNotFoundException("User", id);
}
userRepo.deleteById(id);
}
}
// Constructor Injection (preferred โ no @Autowired needed with single constructor)
@Service
public class OrderService {
private final OrderRepo orderRepo;
private final PaymentService paymentService;
public OrderService(OrderRepo orderRepo, PaymentService paymentService) {
this.orderRepo = orderRepo;
this.paymentService = paymentService;
}
}// DTO with validation
public record CreateUserRequest(
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100)
String name,
@NotBlank
@Email(message = "Invalid email")
String email,
@NotNull
@Min(18) @Max(150)
Integer age,
@Pattern(regexp = "^\\+?[1-9]\\d{1,14}$")
String phone
) {}
// Common validation annotations:
// @NotNull, @NotBlank, @NotEmpty
// @Size(min, max), @Min, @Max
// @Email, @Pattern(regexp)
// @Past, @Future, @PastOrPresent
// @Positive, @PositiveOrZero, @Negative
// Use @Valid in controller to trigger validation
@PostMapping
public UserResponse create(@Valid @RequestBody CreateUserRequest req) {}// Custom Exception
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String resource, Object id) {
super(resource + " not found with id: " + id);
}
}
// Global Exception Handler
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// Handle specific exception
@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(ResourceNotFoundException ex) {
return new ErrorResponse("NOT_FOUND", ex.getMessage());
}
// Handle validation errors
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> handleValidation(MethodArgumentNotValidException ex) {
return ex.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(
FieldError::getField,
fe -> fe.getDefaultMessage() != null ? fe.getDefaultMessage() : "Invalid"
));
}
// Catch-all
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleAll(Exception ex) {
log.error("Unexpected error", ex);
return new ErrorResponse("INTERNAL_ERROR", "Something went wrong");
}
}
public record ErrorResponse(String code, String message) {}@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthFilter jwtAuthFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authManager(AuthenticationConfiguration config)
throws Exception {
return config.getAuthenticationManager();
}
}
// Method-level security
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) { }
@PreAuthorize("#userId == authentication.principal.id")
public User getProfile(Long userId) { }spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: ${DB_USER:postgres}
password: ${DB_PASS:secret}
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: validate # none | validate | update | create-drop
show-sql: true
properties:
hibernate:
format_sql: true
profiles:
active: ${SPRING_PROFILE:dev}
server:
port: 8080
logging:
level:
root: INFO
com.example: DEBUG
org.hibernate.SQL: DEBUG
# Custom properties
app:
jwt:
secret: ${JWT_SECRET}
expiration: 86400000@ConfigurationProperties(prefix = "app.jwt")
public record JwtProperties(
String secret,
long expiration
) {}
// Enable in main class:
@EnableConfigurationProperties(JwtProperties.class)
@SpringBootApplication
public class Application { }// Enable async
@Configuration
@EnableAsync
public class AsyncConfig { }
// Async method
@Async
public CompletableFuture<User> fetchUser(Long id) {
User user = userRepo.findById(id).orElseThrow();
return CompletableFuture.completedFuture(user);
}
// Parallel execution
CompletableFuture<User> userFuture = fetchUser(1L);
CompletableFuture<List<Order>> ordersFuture = fetchOrders(1L);
CompletableFuture.allOf(userFuture, ordersFuture).join();
// Scheduling
@Configuration
@EnableScheduling
public class SchedulerConfig { }
@Scheduled(fixedRate = 60000) // every 60 seconds
public void cleanup() { }
@Scheduled(cron = "0 0 2 * * ?") // daily at 2 AM
public void nightlyJob() { }// Unit Test (Service layer)
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepo;
@Mock
private UserMapper mapper;
@InjectMocks
private UserService userService;
@Test
void findById_returnsUser() {
User user = User.builder().id(1L).name("Bob").build();
when(userRepo.findById(1L)).thenReturn(Optional.of(user));
when(mapper.toResponse(user)).thenReturn(new UserResponse(1L, "Bob"));
UserResponse result = userService.findById(1L);
assertThat(result.name()).isEqualTo("Bob");
verify(userRepo).findById(1L);
}
@Test
void findById_throwsWhenNotFound() {
when(userRepo.findById(99L)).thenReturn(Optional.empty());
assertThrows(ResourceNotFoundException.class,
() -> userService.findById(99L));
}
}
// Integration Test (Controller layer)
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerIT {
@Autowired
private MockMvc mockMvc;
@Test
void createUser_returns201() throws Exception {
mockMvc.perform(post("/api/v1/users")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"name": "Bob", "email": "bob@mail.com", "age": 25}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.name").value("Bob"));
}
}
// Test with @DataJpaTest (repository layer, in-memory DB)
@DataJpaTest
class UserRepositoryTest {
@Autowired UserRepository repo;
@Test
void findByEmail_works() {
repo.save(User.builder().name("Bob").email("bob@mail.com").build());
Optional<User> found = repo.findByEmail("bob@mail.com");
assertThat(found).isPresent();
}
}// Records (Java 16+) โ immutable data classes
public record UserResponse(Long id, String name, String email) {}
// Sealed classes (Java 17+)
public sealed interface Shape
permits Circle, Rectangle, Triangle {}
public record Circle(double radius) implements Shape {}
public record Rectangle(double w, double h) implements Shape {}
// Pattern matching switch (Java 21+)
double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.w() * r.h();
case Triangle t -> 0.5 * t.base() * t.height();
};
}
// Text blocks (Java 15+)
String json = """
{
"name": "Bob",
"age": 25
}
""";
// var (Java 10+)
var users = new ArrayList<User>();
var map = Map.of("key", "value");
// Stream API
List<String> names = users.stream()
.filter(u -> u.getAge() > 18)
.sorted(Comparator.comparing(User::getName))
.map(User::getName)
.distinct()
.toList();
// Optional chaining
String email = findUser(id)
.map(User::getEmail)
.filter(e -> e.contains("@"))
.orElse("N/A");# Multi-stage Dockerfile
FROM eclipse-temurin:21-jdk-alpine AS build
WORKDIR /app
COPY . .
RUN ./mvnw package -DskipTests
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]# docker-compose.yml
services:
app:
build: .
ports:
- "8080:8080"
environment:
- SPRING_PROFILE=prod
- DB_USER=postgres
- DB_PASS=secret
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: mydb
POSTGRES_USER: postgres
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:@Component
public class UserMapper {
public UserResponse toResponse(User user) {
return new UserResponse(user.getId(), user.getName(), user.getEmail());
}
public User toEntity(CreateUserRequest req) {
return User.builder()
.name(req.name())
.email(req.email())
.build();
}
}
// Or use MapStruct for auto-mapping:
@Mapper(componentModel = "spring")
public interface UserMapper {
UserResponse toResponse(User user);
User toEntity(CreateUserRequest req);
}public class UserSpecs {
public static Specification<User> hasStatus(Status status) {
return (root, query, cb) -> cb.equal(root.get("status"), status);
}
public static Specification<User> nameLike(String name) {
return (root, query, cb) ->
cb.like(cb.lower(root.get("name")), "%" + name.toLowerCase() + "%");
}
}
// Usage:
userRepo.findAll(hasStatus(ACTIVE).and(nameLike("bob")), pageable);@Transactional(readOnly = true) for read operations