All topics
Backend · Learning hub

Spring notes for developers

Master Spring with a curated set of 5 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore Backend notes
Spring

Spring Core: IoC & Dependency Injection

Spring: IoC & Dependency Injection Spring's IoC (Inversion of Control) container manages object creation and lifecycle. Beans are objects managed by Spring. Dep

Spring: IoC & Dependency Injection

Spring's IoC (Inversion of Control) container manages object creation and lifecycle. Beans are objects managed by Spring. Dependency Injection wires dependencies automatically — you declare what you need, Spring provides it.

Bean Annotations

// @Component and its specializations
@Component          // generic Spring-managed bean
@Service            // business logic layer (semantic alias of @Component)
@Repository         // data access layer — also translates SQL exceptions
@Controller         // MVC web controller
@RestController     // @Controller + @ResponseBody on all methods

// Configuration class — defines beans explicitly
@Configuration
public class AppConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }

    @Bean
    @Primary                             // preferred when multiple implementations exist
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create()
            .url("jdbc:postgresql://localhost/mydb")
            .build();
    }
}

Dependency Injection

// Constructor injection — RECOMMENDED
@Service
public class UserService {
    private final UserRepository userRepository;
    private final EmailService emailService;

    // Spring automatically injects — @Autowired optional since Spring 4.3
    public UserService(UserRepository userRepository, EmailService emailService) {
        this.userRepository = userRepository;
        this.emailService = emailService;
    }
}

// Field injection — discouraged (hard to test, hides dependencies)
@Autowired
private UserRepository userRepository;  // avoid this

// Qualifier — when multiple beans implement the same interface
@Service
public class NotificationService {
    public NotificationService(
        @Qualifier("emailNotifier") Notifier emailNotifier,
        @Qualifier("smsNotifier")   Notifier smsNotifier) { }
}

// Optional injection
@Autowired(required = false)
private MetricsService metricsService;   // null if not in context

Bean Scopes & Lifecycle

// Scopes
@Scope("singleton")   // default — one instance per container
@Scope("prototype")   // new instance every time injected
@Scope("request")     // one per HTTP request (web apps)
@Scope("session")     // one per HTTP session (web apps)

// Lifecycle callbacks
@Component
public class CacheManager {

    @PostConstruct          // called after DI is complete
    public void init() {
        loadCache();
    }

    @PreDestroy             // called before bean is removed from context
    public void cleanup() {
        flushCache();
    }
}

// Conditional beans
@Bean
@ConditionalOnProperty(name = "feature.email", havingValue = "true")
public EmailService emailService() { return new SmtpEmailService(); }

@Bean
@Profile("development")
public DataSource h2DataSource() { return new EmbeddedDatabaseBuilder().build(); }

@Bean
@Profile("production")
public DataSource postgresDataSource() { /* ... */ }

ApplicationContext & Events

// Access context directly (use sparingly — prefer constructor injection)
@Component
public class BeanFactory implements ApplicationContextAware {
    private ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext ctx) {
        this.context = ctx;
    }

    public <T> T getBean(Class<T> type) { return context.getBean(type); }
}

// Application events — decoupled communication
// Publish
@Service
public class UserService {
    private final ApplicationEventPublisher eventPublisher;

    public void createUser(User user) {
        userRepository.save(user);
        eventPublisher.publishEvent(new UserCreatedEvent(this, user));
    }
}

// Listen
@Component
public class WelcomeEmailListener {

    @EventListener
    @Async                     // handle in separate thread
    public void onUserCreated(UserCreatedEvent event) {
        emailService.sendWelcome(event.getUser());
    }
}
Spring

Spring Boot & Auto-configuration

Spring Boot & Auto-configuration Spring Boot eliminates boilerplate by auto-configuring Spring based on classpath dependencies. Opinionated defaults you can ove

Spring Boot & Auto-configuration

Spring Boot eliminates boilerplate by auto-configuring Spring based on classpath dependencies. Opinionated defaults you can override with application.properties / application.yml.

Project Setup

# start.spring.io — fastest way to bootstrap
curl https://start.spring.io/starter.zip   -d dependencies=web,data-jpa,postgresql,security,validation,actuator   -d type=maven-project   -d language=java   -d bootVersion=3.3.0   -d groupId=com.example   -d artifactId=myapp   -o myapp.zip && unzip myapp.zip

# Or use Spring Initializr IntelliJ plugin / VS Code Spring Boot extension

# Maven commands
./mvnw spring-boot:run        # run in development
./mvnw test                   # run tests
./mvnw package                # build JAR
java -jar target/myapp-0.0.1-SNAPSHOT.jar

application.yml Configuration

server:
  port: 8080
  servlet:
    context-path: /api

spring:
  application:
    name: my-app

  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
    hikari:
      maximum-pool-size: 20
      connection-timeout: 30000

  jpa:
    hibernate:
      ddl-auto: validate        # validate | update | create | create-drop | none
    show-sql: false
    open-in-view: false         # disable OSIV — prevents N+1 in web layer

  data:
    redis:
      host: localhost
      port: 6379

logging:
  level:
    com.example: DEBUG
    org.springframework.security: INFO

# Custom properties (type-safe with @ConfigurationProperties)
app:
  jwt:
    secret: ${JWT_SECRET}
    expiry-minutes: 60
  cors:
    allowed-origins: https://app.example.com

Type-Safe Configuration

// Bind application.yml properties to a class
@ConfigurationProperties(prefix = "app.jwt")
@Validated
public record JwtProperties(
    @NotBlank String secret,
    @Positive int expiryMinutes
) {}

// Enable in main class or @Configuration
@SpringBootApplication
@ConfigurationPropertiesScan
public class MyAppApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyAppApplication.class, args);
    }
}

// Inject like any other bean
@Service
public class TokenService {
    public TokenService(JwtProperties jwt) {
        this.secret = jwt.secret();
    }
}

Actuator & Profiles

# application.yml — Actuator endpoints for monitoring
management:
  endpoints:
    web:
      exposure:
        include: health, info, metrics, prometheus
  endpoint:
    health:
      show-details: when-authorized
  metrics:
    export:
      prometheus:
        enabled: true

# application-dev.yml — development overrides
spring:
  jpa:
    show-sql: true
  h2:
    console:
      enabled: true

logging:
  level:
    root: DEBUG
# Activate profiles
SPRING_PROFILES_ACTIVE=production java -jar app.jar
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev,local

# Build info in /actuator/info
# Add to pom.xml: spring-boot-maven-plugin with build-info goal
Spring

Spring MVC & REST APIs

Spring: Spring MVC & REST APIs REST Controller @RestController @RequestMapping("/api/users") @RequiredArgsConstructor @Validated public class UserController { p

Spring: Spring MVC & REST APIs

REST Controller

@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
@Validated
public class UserController {

    private final UserService userService;

    @GetMapping
    public ResponseEntity<Page<UserDto>> getAll(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "25") @Max(100) int size,
            @RequestParam(required = false) String search) {
        return ResponseEntity.ok(userService.getAll(page, size, search));
    }

    @GetMapping("/{id}")
    public ResponseEntity<UserDto> getById(@PathVariable Long id) {
        return userService.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public UserDto create(@Valid @RequestBody CreateUserRequest request) {
        return userService.create(request);
    }

    @PatchMapping("/{id}")
    public UserDto update(@PathVariable Long id,
                          @Valid @RequestBody UpdateUserRequest request) {
        return userService.update(id, request);
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable Long id) {
        userService.delete(id);
    }
}

Request & Response DTOs

// Record DTOs (Java 16+)
public record UserDto(Long id, String name, String email, LocalDateTime createdAt) {}

public record CreateUserRequest(
    @NotBlank @Size(max = 100) String name,
    @NotBlank @Email String email,
    @NotBlank @Size(min = 8) String password
) {}

// Custom validation annotation
@Constraint(validatedBy = UniqueEmailValidator.class)
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UniqueEmail {
    String message() default "Email already registered";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// ControllerAdvice — global exception handling
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
    public Map<String, Object> handleValidation(MethodArgumentNotValidException ex) {
        var errors = ex.getBindingResult().getFieldErrors().stream()
            .map(e -> Map.of("field", e.getField(), "message", e.getDefaultMessage()))
            .toList();
        return Map.of("status", 422, "errors", errors);
    }

    @ExceptionHandler(EntityNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public Map<String, String> handleNotFound(EntityNotFoundException ex) {
        return Map.of("error", ex.getMessage());
    }
}

Filters, Interceptors & CORS

// CORS configuration
@Configuration
public class CorsConfig {

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        var config = new CorsConfiguration();
        config.setAllowedOrigins(List.of("https://app.example.com"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
        config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
        config.setAllowCredentials(true);
        config.setMaxAge(86400L);

        var source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return source;
    }
}

// HandlerInterceptor — runs around controller methods
@Component
public class RequestLoggingInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
        req.setAttribute("startTime", System.currentTimeMillis());
        return true;  // true = continue chain
    }

    @Override
    public void afterCompletion(HttpServletRequest req, HttpServletResponse res,
                                Object handler, Exception ex) {
        long duration = System.currentTimeMillis() - (long) req.getAttribute("startTime");
        log.info("{} {} → {} in {}ms", req.getMethod(), req.getRequestURI(),
                 res.getStatus(), duration);
    }
}
Spring

Spring Data JPA

Spring: Spring Data JPA Entities & Repositories // Entity @Entity @Table(name = "users", indexes = { @Index(columnList = "email", unique = true) }) @EntityListe

Spring: Spring Data JPA

Entities & Repositories

// Entity
@Entity
@Table(name = "users", indexes = {
    @Index(columnList = "email", unique = true)
})
@EntityListeners(AuditingEntityListener.class)
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 100)
    private String name;

    @Column(nullable = false, unique = true)
    private String email;

    @Enumerated(EnumType.STRING)
    private UserStatus status = UserStatus.ACTIVE;

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
    private List<Post> posts = new ArrayList<>();

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "organization_id")
    private Organization organization;
}

// Repository — Spring generates implementation at runtime
public interface UserRepository extends JpaRepository<User, Long> {

    // Derived queries — Spring generates SQL from method name
    Optional<User> findByEmail(String email);
    List<User> findByStatusAndOrganizationId(UserStatus status, Long orgId);
    boolean existsByEmail(String email);
    long countByStatus(UserStatus status);
    void deleteByEmail(String email);

    // Custom query
    @Query("SELECT u FROM User u WHERE u.name ILIKE %:term% OR u.email ILIKE %:term%")
    Page<User> search(@Param("term") String term, Pageable pageable);

    // Native SQL
    @Query(value = "SELECT * FROM users WHERE created_at > :since", nativeQuery = true)
    List<User> findRecentUsers(@Param("since") LocalDateTime since);

    // Projection — return subset of fields
    List<UserSummary> findByStatus(UserStatus status);
}

Queries & Pagination

// Service layer — use repositories
@Service
@Transactional
public class UserService {

    public Page<UserDto> getAll(int page, int size, String search) {
        var pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
        var userPage = search != null
            ? userRepository.search(search, pageable)
            : userRepository.findAll(pageable);
        return userPage.map(userMapper::toDto);
    }

    // Specification (dynamic filtering)
    public List<User> filter(UserFilter filter) {
        return userRepository.findAll(
            Specification.where(UserSpec.hasStatus(filter.status()))
                .and(UserSpec.inOrganization(filter.orgId()))
                .and(UserSpec.createdAfter(filter.since()))
        );
    }
}

// Specification — composable query predicates
public class UserSpec {
    public static Specification<User> hasStatus(UserStatus status) {
        return (root, query, cb) ->
            status == null ? null : cb.equal(root.get("status"), status);
    }

    public static Specification<User> createdAfter(LocalDate date) {
        return (root, query, cb) ->
            date == null ? null : cb.greaterThan(root.get("createdAt"), date.atStartOfDay());
    }
}

// Avoid N+1 — use JOIN FETCH for associations
@Query("SELECT u FROM User u LEFT JOIN FETCH u.posts WHERE u.id = :id")
Optional<User> findByIdWithPosts(@Param("id") Long id);

// Or use @EntityGraph
@EntityGraph(attributePaths = {"posts", "organization"})
Optional<User> findWithDetailsById(Long id);

Transactions & Migrations

// @Transactional — Spring manages transaction boundaries
@Service
@Transactional(readOnly = true)   // default: read-only (optimizes SELECT)
public class UserService {

    @Transactional                // override: writable
    public User create(CreateUserRequest req) {
        var user = new User(req.name(), req.email());
        userRepository.save(user);
        emailService.sendWelcome(user);  // if this throws, user creation rolls back
        return user;
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void auditLog(String action) { }  // always runs in own transaction

    @Transactional(noRollbackFor = EmailException.class)
    public void createWithoutRollingBackOnEmail(CreateUserRequest req) { }
}

// Flyway (recommended) or Liquibase — database migrations
// Add flyway-core dependency; create src/main/resources/db/migration/
// V1__init_schema.sql, V2__add_users_table.sql, V3__add_posts.sql

# application.yml
spring:
  flyway:
    enabled: true
    locations: classpath:db/migration
    baseline-on-migrate: true
Spring

Spring Security

Spring: Spring Security Security Configuration (Spring Security 6) @Configuration @EnableWebSecurity @EnableMethodSecurity public class SecurityConfig { @Bean p

Spring: Spring Security

Security Configuration (Spring Security 6)

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())                  // stateless JWT API
            .sessionManagement(sm -> sm
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/actuator/health").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated())
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint((req, res, e) ->
                    res.sendError(401, "Unauthorized"))
                .accessDeniedHandler((req, res, e) ->
                    res.sendError(403, "Forbidden")))
            .build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }

    @Bean
    public AuthenticationManager authManager(UserDetailsService uds) {
        var provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(uds);
        provider.setPasswordEncoder(passwordEncoder());
        return new ProviderManager(provider);
    }
}

JWT Filter

@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws IOException, ServletException {
        var authHeader = request.getHeader("Authorization");
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            chain.doFilter(request, response);
            return;
        }

        var token = authHeader.substring(7);
        var username = jwtService.extractUsername(token);

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            var userDetails = userDetailsService.loadUserByUsername(username);

            if (jwtService.isTokenValid(token, userDetails)) {
                var auth = new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities());
                auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(auth);
            }
        }
        chain.doFilter(request, response);
    }
}

UserDetailsService & Method Security

// UserDetailsService — loads user from DB for authentication
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        return userRepository.findByEmail(email)
            .map(user -> org.springframework.security.core.userdetails.User
                .withUsername(user.getEmail())
                .password(user.getPassword())
                .roles(user.getRole().name())   // ROLE_ prefix added automatically
                .accountLocked(!user.isActive())
                .build())
            .orElseThrow(() -> new UsernameNotFoundException("User not found: " + email));
    }
}

// Method-level security (requires @EnableMethodSecurity)
@RestController
public class AdminController {

    @GetMapping("/api/admin/users")
    @PreAuthorize("hasRole('ADMIN')")                 // annotation-based
    public List<User> getAllUsers() { }

    @PutMapping("/api/users/{id}")
    @PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
    public User updateUser(@PathVariable Long id, @RequestBody UpdateUserRequest req) { }

    @DeleteMapping("/api/posts/{id}")
    @PreAuthorize("@postService.isOwner(#id, authentication.name)")  // SpEL with bean
    public void deletePost(@PathVariable Long id) { }
}

// Get current user anywhere
public User getCurrentUser() {
    var auth = SecurityContextHolder.getContext().getAuthentication();
    return (User) auth.getPrincipal();
}

Keep your Spring knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever