Spring Boot
Annotations, auto-configuration, dependency injection, REST, data access, and testing.
At a Glance
@SpringBootApplication — Entry point. Combines @Configuration, @EnableAutoConfiguration, and @ComponentScan. Place on your main class. @RestController — Marks a class as a REST controller. Every method returns a response body (no view resolution). Combines @Controller + @ResponseBody. @Service, @Repository, @Component — Stereotype annotations that register beans in the application context. Use @Service for business logic, @Repository for data access. @Autowired — Dependency injection. Prefer constructor injection (implicit when single constructor). Field injection works but is harder to test. @Configuration + @Bean — Java-based config. Methods annotated @Bean inside @Configuration classes produce managed beans. application.properties / application.yml — Externalized config. Supports profiles (application-dev.yml), environment variables, and @Value injection. @Transactional — Declarative transaction management. Place on service methods. Rolls back on unchecked exceptions by default. - Spring Data JPA — Repository interfaces with automatic query derivation from method names. Saves massive boilerplate for CRUD operations.
@SpringBootTest — Integration test annotation. Boots the full application context. Use @WebMvcTest for controller-only slice tests.
Typical Project Structure
src/main/java/com/example/myapp/
MyappApplication.java // @SpringBootApplication
controller/
UserController.java // @RestController
service/
UserService.java // @Service
repository/
UserRepository.java // extends JpaRepository
model/
User.java // @Entity
config/
SecurityConfig.java // @Configuration
dto/
UserDto.java // record or POJO
src/main/resources/
application.yml
application-dev.yml
application-prod.yml
src/test/java/com/example/myapp/
controller/
UserControllerTest.java // @WebMvcTest
service/
UserServiceTest.java // @SpringBootTest or unit test
Dependency Injection
// Constructor injection (preferred — implicit @Autowired with single constructor)
@Service
public class UserService {
private final UserRepository repo;
private final EmailService email;
public UserService(UserRepository repo, EmailService email) {
this.repo = repo;
this.email = email;
}
}
// With Lombok
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository repo;
private final EmailService email;
}
// @Bean method in config class
@Configuration
public class AppConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean
@Profile("prod")
public DataSource prodDataSource() {
// ...
}
}
Bean Scopes
| Scope | Lifecycle | Use Case |
singleton (default) | One instance per application context | Stateless services, repos |
prototype | New instance per injection | Stateful beans |
request | One per HTTP request | Request-scoped data |
session | One per HTTP session | Session-scoped data |
REST Controllers
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService service;
public UserController(UserService service) {
this.service = service;
}
@GetMapping
public List<UserDto> list() {
return service.findAll();
}
@GetMapping("/{id}")
public UserDto get(@PathVariable Long id) {
return service.findById(id);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public UserDto create(@Valid @RequestBody CreateUserRequest req) {
return service.create(req);
}
@PutMapping("/{id}")
public UserDto update(@PathVariable Long id, @Valid @RequestBody UpdateUserRequest req) {
return service.update(id, req);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
service.delete(id);
}
}
Common Annotations
| Annotation | Purpose |
@GetMapping, @PostMapping, @PutMapping, @DeleteMapping | HTTP method mapping |
@PathVariable | Extract from URL path (/users/{id}) |
@RequestParam | Query parameter (?page=1) |
@RequestBody | Deserialize JSON body to object |
@Valid | Trigger Bean Validation on the parameter |
@ResponseStatus | Set HTTP status code |
@RequestHeader | Extract an HTTP header |
Exception Handling
// Global exception handler
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(UserNotFoundException ex) {
return new ErrorResponse(ex.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleValidation(MethodArgumentNotValidException ex) {
List<String> errors = ex.getBindingResult().getFieldErrors().stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.toList();
return new ErrorResponse("Validation failed", errors);
}
}
record ErrorResponse(String message, List<String> details) {
ErrorResponse(String message) { this(message, List.of()); }
}
Configuration
# application.yml
server:
port: 8080
spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: ${DB_USER:postgres}
password: ${DB_PASS:secret}
jpa:
hibernate:
ddl-auto: validate
show-sql: false
profiles:
active: ${SPRING_PROFILE:dev}
app:
jwt-secret: ${JWT_SECRET}
page-size: 20
// Bind custom properties to a type-safe object
@ConfigurationProperties(prefix = "app")
public record AppProperties(
String jwtSecret,
int pageSize
) {}
// Enable it
@SpringBootApplication
@EnableConfigurationProperties(AppProperties.class)
public class MyappApplication {}
// Inject it
@Service
public class TokenService {
private final AppProperties props;
public TokenService(AppProperties props) {
this.props = props;
}
}
Spring Data JPA
// Entity
@Entity
@Table(name = "users")
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(unique = true, nullable = false)
private String email;
// getters, setters (or use Lombok @Data)
}
// Repository — CRUD for free
public interface UserRepository extends JpaRepository<User, Long> {
// Derived query from method name
Optional<User> findByEmail(String email);
List<User> findByNameContainingIgnoreCase(String fragment);
// Custom JPQL
@Query("SELECT u FROM User u WHERE u.email LIKE %:domain")
List<User> findByEmailDomain(@Param("domain") String domain);
// Native SQL
@Query(value = "SELECT * FROM users WHERE created_at > :since", nativeQuery = true)
List<User> findRecentUsers(@Param("since") LocalDate since);
boolean existsByEmail(String email);
long countByNameContaining(String fragment);
}
Query Method Keywords
| Keyword | Example | SQL |
findBy | findByName(String) | WHERE name = ? |
And / Or | findByNameAndAge | WHERE name = ? AND age = ? |
OrderBy | findByAgeOrderByNameAsc | ORDER BY name ASC |
Between | findByAgeBetween(int, int) | WHERE age BETWEEN ? AND ? |
LessThan / GreaterThan | findByAgeLessThan(int) | WHERE age < ? |
Like / Containing | findByNameContaining(String) | WHERE name LIKE %?% |
In | findByIdIn(List<Long>) | WHERE id IN (?) |
IsNull / IsNotNull | findByDeletedAtIsNull() | WHERE deleted_at IS NULL |
Top / First | findTop5ByOrderByCreatedAtDesc | LIMIT 5 |
@Transactional
@Service
public class OrderService {
private final OrderRepository orderRepo;
private final PaymentService paymentService;
@Transactional // rolls back on unchecked exceptions
public Order placeOrder(OrderRequest req) {
Order order = orderRepo.save(new Order(req));
paymentService.charge(order); // if this throws, order is rolled back
return order;
}
@Transactional(readOnly = true) // optimization hint for read-only queries
public List<Order> listOrders() {
return orderRepo.findAll();
}
@Transactional(rollbackFor = Exception.class) // roll back on checked exceptions too
public void importData() throws IOException {
// ...
}
}
| Attribute | Default | Purpose |
readOnly | false | Hint for read-only optimization (flush mode, replica routing) |
rollbackFor | Unchecked only | Also roll back on specified checked exceptions |
propagation | REQUIRED | Join existing tx or create new. REQUIRES_NEW always creates new. |
isolation | DB default | Set isolation level (READ_COMMITTED, SERIALIZABLE, etc.) |
timeout | None | Seconds before tx timeout |
Testing
Integration Test
@SpringBootTest
class UserServiceTest {
@Autowired UserService service;
@Test
void shouldCreateUser() {
var user = service.create(new CreateUserRequest("Alice", "alice@test.com"));
assertThat(user.name()).isEqualTo("Alice");
}
}
Controller Slice Test
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired MockMvc mvc;
@MockitoBean UserService service;
@Test
void shouldReturnUser() throws Exception {
when(service.findById(1L)).thenReturn(new UserDto(1L, "Alice"));
mvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Alice"));
}
@Test
void shouldValidateInput() throws Exception {
mvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("{}"))
.andExpect(status().isBadRequest());
}
}
Test Annotations
| Annotation | Scope | Use Case |
@SpringBootTest | Full context | Integration tests, service layer tests |
@WebMvcTest | Web layer only | Controller tests with MockMvc |
@DataJpaTest | JPA layer only | Repository tests with embedded DB |
@MockitoBean | Replaces bean with mock | Isolating the layer under test |
@TestPropertySource | Override config | Test-specific property values |
Profiles
# Activate via env variable
SPRING_PROFILES_ACTIVE=prod java -jar app.jar
# Or via command line
java -jar app.jar --spring.profiles.active=prod
# Profile-specific config files
application-dev.yml # loaded when profile is "dev"
application-prod.yml # loaded when profile is "prod"
// Profile-conditional beans
@Configuration
public class DataSourceConfig {
@Bean
@Profile("dev")
public DataSource devDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2).build();
}
@Bean
@Profile("prod")
public DataSource prodDataSource() {
// HikariCP pool with prod credentials
return DataSourceBuilder.create().build();
}
}
Actuator
Production-ready monitoring endpoints.
# application.yml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: when_authorized
| Endpoint | URL | Purpose |
| Health | /actuator/health | Liveness/readiness checks (DB, disk, custom) |
| Info | /actuator/info | Build info, git commit, custom info |
| Metrics | /actuator/metrics | JVM, HTTP, custom metrics via Micrometer |
| Env | /actuator/env | Configuration properties (sanitized) |
| Loggers | /actuator/loggers | View and change log levels at runtime |