Spring Boot

Annotations, auto-configuration, dependency injection, REST, data access, and testing.

At a Glance

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

ScopeLifecycleUse Case
singleton (default)One instance per application contextStateless services, repos
prototypeNew instance per injectionStateful beans
requestOne per HTTP requestRequest-scoped data
sessionOne per HTTP sessionSession-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

AnnotationPurpose
@GetMapping, @PostMapping, @PutMapping, @DeleteMappingHTTP method mapping
@PathVariableExtract from URL path (/users/{id})
@RequestParamQuery parameter (?page=1)
@RequestBodyDeserialize JSON body to object
@ValidTrigger Bean Validation on the parameter
@ResponseStatusSet HTTP status code
@RequestHeaderExtract 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

KeywordExampleSQL
findByfindByName(String)WHERE name = ?
And / OrfindByNameAndAgeWHERE name = ? AND age = ?
OrderByfindByAgeOrderByNameAscORDER BY name ASC
BetweenfindByAgeBetween(int, int)WHERE age BETWEEN ? AND ?
LessThan / GreaterThanfindByAgeLessThan(int)WHERE age < ?
Like / ContainingfindByNameContaining(String)WHERE name LIKE %?%
InfindByIdIn(List<Long>)WHERE id IN (?)
IsNull / IsNotNullfindByDeletedAtIsNull()WHERE deleted_at IS NULL
Top / FirstfindTop5ByOrderByCreatedAtDescLIMIT 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 {
        // ...
    }
}
AttributeDefaultPurpose
readOnlyfalseHint for read-only optimization (flush mode, replica routing)
rollbackForUnchecked onlyAlso roll back on specified checked exceptions
propagationREQUIREDJoin existing tx or create new. REQUIRES_NEW always creates new.
isolationDB defaultSet isolation level (READ_COMMITTED, SERIALIZABLE, etc.)
timeoutNoneSeconds 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

AnnotationScopeUse Case
@SpringBootTestFull contextIntegration tests, service layer tests
@WebMvcTestWeb layer onlyController tests with MockMvc
@DataJpaTestJPA layer onlyRepository tests with embedded DB
@MockitoBeanReplaces bean with mockIsolating the layer under test
@TestPropertySourceOverride configTest-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
EndpointURLPurpose
Health/actuator/healthLiveness/readiness checks (DB, disk, custom)
Info/actuator/infoBuild info, git commit, custom info
Metrics/actuator/metricsJVM, HTTP, custom metrics via Micrometer
Env/actuator/envConfiguration properties (sanitized)
Loggers/actuator/loggersView and change log levels at runtime