Java for Spring An opinionated guide
Java for Spring
An opinionated guide built backwards from Spring's source code
Interfaces · Generics · Lambdas · Stream API · Optional · Annotations · CompletableFuture · Lombok
8 Layers · Spring Bridge sections · Real-world patterns
The philosophy behind this document
Most Java courses teach Java. This document teaches Java through the lens of what Spring Boot actually needs. Every
concept here was chosen because it appears directly in Spring's source code, in your application code, or in the mental
model Spring developers share. If something doesn't serve that goal, it isn't here. The order matters: we start with the
abstractions Spring is built on (interfaces, generics), then add the modern Java features that make Spring code readable
(lambdas, streams, optional), and finish with the tooling that defines the ecosystem (Maven, Lombok, annotations). By the
end, Spring won't feel like magic — it will feel like Java.
LAYER 1
CONTRACTS — Interfaces & Generics
Why first? Spring is built on interfaces. JpaRepository, ApplicationContext, BeanFactory — all interfaces. Generics make them type-safe.
Without these, Spring's source code is unreadable.
■ Interface — the contract
// An interface defines WHAT, not HOW
public interface UserRepository {
User findById(Long id);
List<User> findAll();
void save(User user);
}
// Implementation defines HOW
public class PostgresUserRepository implements UserRepository {
@Override
public User findById(Long id) {
// actual SQL query here
return ...
}
}
// Caller only knows the contract, not the implementation
UserRepository repo = new PostgresUserRepository();
// — Spring injects the right impl automatically
■ Spring wires implementations to interfaces automatically — this is Dependency Injection.
■ Default & Static methods in interfaces (Java 8+)
public interface Auditable {
LocalDateTime getCreatedAt();
// default — optional override, has implementation
default boolean isRecent() {
return getCreatedAt().isAfter([Link]().minusDays(7));
—1—
Java for Spring An opinionated guide
// static — utility, belongs to interface
static Auditable of(LocalDateTime time) { ... }
}
■ Generics — type-safe contracts
// Without generics — dangerous, requires cast
public interface Repository {
Object findById(Object id); // cast nightmare
}
// With generics — safe and expressive
public interface Repository<T, ID> {
T findById(ID id); // T resolved at compile time
List<T> findAll();
void save(T entity);
}
// Usage — T=User, ID=Long
public class UserRepo implements Repository<User, Long> {
@Override
public User findById(Long id) { ... }
}
■ Bounded type parameters
// T must be a Number (or subclass)
public <T extends Number> double sum(List<T> list) {
return [Link]().mapToDouble(Number::doubleValue).sum();
}
// Wildcard — unknown type
void printAll(List<?> items) {
[Link]([Link]::println);
}
// Upper bounded wildcard — read-only list of Numbers
double total(List<? extends Number> nums) { ... }
// Lower bounded wildcard — writable list of Numbers
void addNumbers(List<? super Integer> nums) { ... }
Type Param Pattern Meaning Spring Example
Any type Repository
T is a Foo or subclass CrudRepository
Unknown type (read-only) List in reflection APIs
Producer — read from it Collection
Consumer — write to it Comparator
■ JpaRepository — T=User entity, ID=Long primary key. Memorise this pattern.
SPRING BRIDGE — Interfaces in Spring
// You write the interface
public interface UserService {
UserDto getUser(Long id);
UserDto createUser(CreateUserRequest req);
}
// You write the implementation
@Service
—2—
Java for Spring An opinionated guide
public class UserServiceImpl implements UserService {
private final UserRepository repo;
// constructor injection — preferred over @Autowired field
public UserServiceImpl(UserRepository repo) {
[Link] = repo;
}
@Override
public UserDto getUser(Long id) {
return [Link](id)
.map(UserMapper::toDto)
.orElseThrow(() -> new UserNotFoundException(id));
}
}
// Spring wires UserRepository -> UserServiceImpl -> Controller
// You never call 'new' — Spring manages object lifecycle
—3—
Java for Spring An opinionated guide
LAYER 2
OBJECT LIFECYCLE — Construction, Immutability & Patterns
Spring creates objects for you (beans). Understanding how objects are constructed, copied, and made immutable is critical for writing safe,
testable Spring components.
■ Constructors & Immutability
// Mutable class — dangerous in concurrent Spring context
public class UserDto {
public String name;
public String email;
}
// Immutable class — thread-safe, Spring-friendly
public final class UserDto {
private final String name;
private final String email;
public UserDto(String name, String email) {
[Link] = name;
[Link] = email;
}
public String getName() { return name; }
public String getEmail() { return email; }
// 'wither' pattern — return new instance with changed field
public UserDto withEmail(String email) {
return new UserDto([Link], email);
}
}
■ Immutable objects = no synchronization needed = better performance in Spring.
■ Builder Pattern — for objects with many optional fields
public class HttpRequest {
private final String url;
private final String method;
private final Map<String,String> headers;
private final String body;
private final int timeout;
private HttpRequest(Builder b) {
[Link] = [Link];
[Link] = [Link];
[Link] = [Link]([Link]);
[Link] = [Link];
[Link] = [Link];
}
public static class Builder {
private String url;
private String method = "GET";
private Map<String,String> headers = new HashMap<>();
private String body;
private int timeout = 30;
public Builder url(String url) { [Link] = url; return this; }
public Builder method(String m) { [Link] = m; return this; }
public Builder header(String k,String v){ [Link](k,v); return this; }
public Builder body(String b) { [Link] = b; return this; }
public Builder timeout(int t) { [Link] = t; return this; }
public HttpRequest build() { return new HttpRequest(this); }
—4—
Java for Spring An opinionated guide
}
}
// Usage — readable, no positional argument confusion
HttpRequest req = new [Link]()
.url("[Link]
.method("POST")
.header("Content-Type", "application/json")
.body(json)
.timeout(60)
.build();
■ Lombok @Builder generates all this boilerplate — see Layer 8.
■ Records (Java 16+) — immutable data carriers
// Record = immutable class + constructor + getters + equals/hashCode/toString
public record UserDto(String name, String email, int age) {}
// Equivalent to ~40 lines of manual code above
UserDto dto = new UserDto("Alice", "alice@[Link]", 30);
[Link](); // accessor (not getName()!)
[Link]();
// Compact canonical constructor — add validation
public record UserDto(String name, String email, int age) {
public UserDto {
[Link](name, "name required");
[Link](email, "email required");
if (age < 0) throw new IllegalArgumentException("age >= 0");
}
}
■ Spring Boot 3+ supports Records as @RequestBody DTOs and @ConfigurationProperties.
■ Static Factory Methods — preferred over 'new'
public class Money {
private final BigDecimal amount;
private final Currency currency;
// Private constructor
private Money(BigDecimal amount, Currency currency) { ... }
// Named factory methods — intent is clear
public static Money of(BigDecimal amount, Currency currency) {
if ([Link]([Link]) < 0)
throw new IllegalArgumentException("Negative money");
return new Money(amount, currency);
}
public static Money zero(Currency currency) {
return new Money([Link], currency);
}
}
Money price = [Link](new BigDecimal("9.99"), [Link]);
■ You'll see this pattern everywhere in Spring: [Link](...), [Link](...)
—5—
Java for Spring An opinionated guide
LAYER 3
MODERN JAVA — Lambdas, Streams & Optional
This layer is non-negotiable for Spring. Spring Data returns Stream. Spring Security uses functional predicates. Every Repository method
returns Optional. Skip this and Spring code is unreadable.
■ Functional Interfaces — the foundation of lambdas
Interface Method In / Out Spring Use
Predicate test(T t) -> boolean T -> bool @PreAuthorize conditions
Function apply(T t) -> R T -> R .map() in streams
Consumer accept(T t) -> void T -> void forEach, event handlers
Supplier get() -> T () -> T Lazy bean initialization
BiFunction apply(T,U) -> R T,U -> R Merging, combining
UnaryOperator apply(T t) -> T T -> T Middleware chains
■ Lambda syntax
// Verbose anonymous class
Predicate<String> isLong = new Predicate<String>() {
@Override public boolean test(String s) { return [Link]() > 5; }
};
// Lambda — same thing, 1 line
Predicate<String> isLong = s -> [Link]() > 5;
// Multi-param
BiFunction<Integer,Integer,Integer> add = (a, b) -> a + b;
// Multi-line body
Function<String, String> clean = input -> {
String trimmed = [Link]();
return [Link]();
};
// Method reference — even cleaner
Function<String,Integer> parse = Integer::parseInt; // static
Function<String,String> upper = String::toUpperCase; // instance
Supplier<List<String>> newList = ArrayList::new; // constructor
■ Stream API — process collections declaratively
List<User> users = [Link](); // imagine this is your database result
// PIPELINE: source -> intermediate ops -> terminal op
List<String> adminEmails = [Link]() // 1. source
.filter(u -> [Link]() == [Link]) // 2. filter
.filter(u -> [Link]()) // 2. chain filters
.sorted([Link](User::getName)) // 2. sort
.map(User::getEmail) // 2. transform
.distinct() // 2. deduplicate
.collect([Link]()); // 3. terminal
// Other terminals
long count = [Link]().filter(User::isActive).count();
boolean any = [Link]().anyMatch(u -> [Link]() > 60);
boolean all = [Link]().allMatch(User::isVerified);
Optional<User> first = [Link]().filter(...).findFirst();
// Reducing
int totalAge = [Link]().mapToInt(User::getAge).sum();
double avgAge = [Link]().mapToInt(User::getAge).average().orElse(0);
—6—
Java for Spring An opinionated guide
// Grouping
Map<Role, List<User>> byRole = [Link]()
.collect([Link](User::getRole));
// Joining strings
String names = [Link]()
.map(User::getName)
.collect([Link](", "));
■ Spring Data Page and Slice wrap query results — call .stream() on .getContent().
■ Optional — the null eliminator
// The problem: NullPointerException
User user = [Link](id); // returns null if not found
String name = [Link](); // CRASH if user is null
// The solution: Optional<T>
Optional<User> opt = [Link](id); // never null
// 1. If present, do something
[Link](u -> sendWelcomeEmail([Link]()));
// 2. Map — transform if present
Optional<String> email = [Link](User::getEmail);
// 3. OrElse — default value
User user = [Link]([Link]);
// 4. OrElseThrow — exception if absent
User user = [Link](() -> new UserNotFoundException(id));
// 5. FlatMap — Optional inside Optional
Optional<Address> addr = [Link](User::getOptionalAddress);
// 6. Filter
Optional<User> admin = [Link](u -> [Link]() == [Link]);
// Creating Optional
[Link](value); // value must NOT be null
[Link](value); // ok if null
[Link](); // explicitly empty
■ Never call [Link]() without checking isPresent() first — defeats the purpose.
■ All Spring Data findById() methods return Optional. Chain .orElseThrow() directly.
LAYER 4
ERRORS & RESOURCES — Exceptions & try-with-resources
Spring's @ExceptionHandler is just Java exceptions with annotations. Understanding checked vs unchecked determines your API design.
■ Checked vs Unchecked — the most important distinction
Type Extends Must handle? When to use Spring uses
Rare — Spring
External resources: IO, DB,
Checked Exception Yes (compile) prefers
network
unchecked
Programming errors, domain @ResponseStatus
Unchecked RuntimeException No
violations exceptions
JVM failures: OutOfMemory,
Error Error Never catch Never touch
StackOverflow
■ Custom exceptions — the Spring way
—7—
Java for Spring An opinionated guide
// Base domain exception — unchecked
public class DomainException extends RuntimeException {
private final String errorCode;
public DomainException(String errorCode, String message) {
super(message);
[Link] = errorCode;
}
public DomainException(String errorCode, String message, Throwable cause) {
super(message, cause);
[Link] = errorCode;
}
public String getErrorCode() { return errorCode; }
}
// Specific exceptions extend base
public class UserNotFoundException extends DomainException {
public UserNotFoundException(Long id) {
super("USER_NOT_FOUND", "User not found: " + id);
}
}
public class EmailAlreadyExistsException extends DomainException {
public EmailAlreadyExistsException(String email) {
super("EMAIL_EXISTS", "Email already registered: " + email);
}
}
■ try-with-resources — always for IO
// AutoCloseable resources close automatically — even on exception
try (var conn = [Link]();
var stmt = [Link]("SELECT * FROM users WHERE id=?");
var reader = new BufferedReader(new FileReader("[Link]"))) {
[Link](1, userId);
ResultSet rs = [Link]();
// process rs...
} catch (SQLException e) {
throw new DomainException("DB_ERROR", "Query failed", e);
}
// conn, stmt, reader all closed here — no finally block needed
■ Any class implementing AutoCloseable works in try-with-resources.
■ Spring's @Transactional handles connection lifecycle — you rarely open connections manually.
■ Global exception handling in Spring (preview)
// This is pure Java with Spring annotations on top
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler([Link])
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(UserNotFoundException ex) {
return new ErrorResponse([Link](), [Link]());
}
@ExceptionHandler([Link])
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleDomain(DomainException ex) {
return new ErrorResponse([Link](), [Link]());
}
}
—8—
Java for Spring An opinionated guide
LAYER 5
COLLECTIONS — The Right Tool for the Right Job
Spring repositories return List, Set, Map, Page. Knowing when to use which collection avoids N+1 queries and performance bugs.
■ Collection hierarchy (what matters)
Interface Best Implementation Ordered? Duplicates? Null? Spring use
findAll(), request
List ArrayList Yes (index) Yes Yes
bodies
HashSet /
Set No / insert No One Unique tags, roles
LinkedHashSet
HashMap / Grouping, lookup
Map No / insert Keys no One
LinkedHashMap tables
Event queues, batch
Queue ArrayDeque FIFO Yes No
jobs
Undo stacks, browser
Deque ArrayDeque Both ends Yes No
history
■ Choosing the right List
// ArrayList — get O(1), add O(1) amortized, remove middle O(n)
// — use for: most cases, iteration, random access
List<User> users = new ArrayList<>();
// LinkedList — add/remove head O(1), get O(n)
// — use for: frequent insertions/deletions at front/back
Deque<Task> taskQueue = new LinkedList<>();
// Unmodifiable — prevent accidental mutation (Spring config)
List<String> allowed = [Link]("GET", "POST", "PUT"); // Java 9+
Map<String,String> config = [Link]("host","localhost","port","8080");
■ Map operations — what you'll actually use
Map<Long, User> cache = new HashMap<>();
// Put / get / remove
[Link]([Link](), user);
User u = [Link](id); // null if absent
User u = [Link](id, [Link]);
// Compute patterns — avoid double lookup
[Link](id, loadFromDb(id));
[Link](id, k -> loadFromDb(k)); // lambda, lazy
[Link](id, (k,v) -> [Link](now()));
// Merge — combine old and new value
[Link](word, 1, Integer::sum);
// Iteration
[Link]((id, user) -> [Link]("{}: {}", id, [Link]()));
[Link]().stream()
.filter(e -> [Link]().isActive())
.map([Link]::getValue)
.collect([Link]());
■ Spring's ApplicationContext is essentially a Map of beans.
LAYER 6
ANNOTATIONS & REFLECTION — How Spring Magic Works
—9—
Java for Spring An opinionated guide
Spring annotations are not magic. They are Java annotations that Spring's reflection-based framework reads at startup. Understanding this
demystifies @Autowired, @Transactional, and @RequestMapping entirely.
■ Creating custom annotations
import [Link].*;
// 1. Define the annotation
@Retention([Link]) // visible at runtime via reflection
@Target([Link]) // only on methods
public @interface LogExecutionTime {
String value() default ""; // optional description
}
// 2. Use it
public class UserService {
@LogExecutionTime("find user")
public User findById(Long id) { ... }
}
// 3. Read it via reflection (Spring does this for you with AOP)
Method method = [Link]("findById", [Link]);
if ([Link]([Link])) {
LogExecutionTime ann = [Link]([Link]);
long start = [Link]();
// invoke method...
[Link]("{} took {}ms", [Link](), ([Link]()-start)/1_000_000);
}
■ Reflection basics — what Spring does internally
Class<?> clazz = [Link];
// Inspect fields
for (Field field : [Link]()) {
[Link](true); // access private fields
[Link]([Link]() + ": " + [Link]());
}
// Create instance dynamically
Object instance = [Link]().newInstance();
// Invoke method dynamically
Method m = [Link]("getName");
String name = (String) [Link](instance);
// Inspect annotations on a class
if ([Link]([Link])) {
Entity ann = [Link]([Link]);
String tableName = [Link]();
}
■ Spring scans your classpath at startup, reads all annotations via reflection, and builds the bean context.
■ Reflection is slow — Spring caches the results. Don't use it in hot paths yourself.
@Retention Visibility
SOURCE Compile only — discarded in .class file (@Override, @SuppressWarnings)
CLASS In .class file — not at runtime (default)
RUNTIME At runtime via reflection — required for Spring annotations
— 10 —
Java for Spring An opinionated guide
LAYER 7
CONCURRENCY — Threading, Async & CompletableFuture
Spring handles many requests simultaneously. Understanding thread safety prevents data corruption. CompletableFuture is how modern
Spring apps do async operations without blocking threads.
■ Thread safety — the core problem
// PROBLEM — shared mutable state
public class Counter {
private int count = 0; // shared between threads
// NOT thread-safe — read-modify-write is 3 operations
public void increment() { count++; }
}
// SOLUTION 1: synchronized method
public synchronized void increment() { count++; }
// SOLUTION 2: AtomicInteger — lock-free, faster
private AtomicInteger count = new AtomicInteger(0);
public void increment() { [Link](); }
// SOLUTION 3: avoid shared state entirely (best)
// Spring beans are stateless — move state to DB/Redis
■ Spring beans are singletons — never store request-scoped data in instance fields!
■ CompletableFuture — modern async
import [Link];
// 1. Run async task
CompletableFuture<User> future = [Link](() -> {
return [Link](id); // runs in ForkJoinPool
});
// 2. Chain transformations (non-blocking)
CompletableFuture<UserDto> dtoFuture = future
.thenApply(UserMapper::toDto) // transform result
.thenApply(dto -> [Link]("USER"));
// 3. Combine two futures
CompletableFuture<User> userFuture = fetchUser(id);
CompletableFuture<Profile> proFuture = fetchProfile(id);
CompletableFuture<FullUser> combined =
[Link](proFuture, FullUser::new);
// 4. Run all and wait
[Link](f1, f2, f3).join();
// 5. Exception handling
future
.exceptionally(ex -> [Link])
.whenComplete((result, ex) -> [Link]("Done: {}", result));
// 6. Get result (blocking — avoid in reactive apps)
User user = [Link](5, [Link]);
■ Annotate method with @Async — Spring wraps it in CompletableFuture automatically.
■ Executor Service — control your thread pools
// Don't use raw Thread — use ExecutorService
ExecutorService pool = [Link](4);
Future<String> f = [Link](() -> fetchFromApi());
— 11 —
Java for Spring An opinionated guide
String result = [Link](); // blocks
[Link](); // graceful shutdown
[Link](30, [Link]);
■ Configure Spring's TaskExecutor in [Link]: [Link]-size=4
LAYER 8
TOOLING — Maven, Lombok & Project Structure
Before you write a single line of Spring, you need to understand the project structure. Spring Initializr generates this — understanding it
means you can debug dependency issues and add features confidently.
■ Maven — [Link] structure
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="[Link]
<modelVersion>4.0.0</modelVersion>
<!-- Your project coordinates -->
<groupId>[Link]</groupId>
<artifactId>my-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<!-- Inherits Spring Boot's managed dependency versions -->
<parent>
<groupId>[Link]</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<dependencies>
<!-- Web: @RestController, HTTP server -->
<dependency>
<groupId>[Link]</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Database: JPA + Hibernate -->
<dependency>
<groupId>[Link]</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Lombok: eliminates boilerplate -->
<dependency>
<groupId>[Link]</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
■ Lombok — eliminate boilerplate
Annotation Generates Replaces
@Getter All getters 10+ manual methods
@Setter All setters 10+ manual methods
@ToString toString() Manual toString
@EqualsAndHashCode equals() + hashCode() ~20 lines
Constructor with no
@NoArgsConstructor Boilerplate
args
— 12 —
Java for Spring An opinionated guide
Constructor with all
@AllArgsConstructor Boilerplate
fields
Constructor for final
@RequiredArgsConstructor Spring constructor DI
fields
@Getter+@Setter+@ToStri
@Data Entire DTO/Entity boilerplate
ng+@EH+@RA
@Builder Full builder pattern 60+ lines of builder code
private static final
@Slf4j Logger declaration
Logger log
// Without Lombok — 60 lines
// With Lombok — 8 lines
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
private Long id;
private String name;
private String email;
private Role role;
}
// Usage
UserDto dto = [Link]()
.id(1L)
.name("Alice")
.email("alice@[Link]")
.role([Link])
.build();
@Service
@RequiredArgsConstructor // generates constructor for final fields
@Slf4j // injects log variable
public class UserService {
private final UserRepository userRepo; // injected via constructor
private final EmailService emailSvc;
public UserDto create(CreateUserRequest req) {
[Link]("Creating user: {}", [Link]());
// ...
}
}
■ Avoid @Data on @Entity classes — it can cause infinite loops via toString() with lazy relations.
■ Use @Getter @Setter separately on JPA entities for more control.
■ Spring Boot project structure
src/
main/
java/com/example/myapi/
[Link] // @SpringBootApplication entry point
controller/
[Link] // @RestController — HTTP layer
service/
[Link] // interface — contract
[Link] // @Service — business logic
repository/
[Link] // @Repository extends JpaRepository
domain/
[Link] // @Entity — database model
[Link] // @Data — transfer object
exception/
[Link] // custom exceptions
— 13 —
Java for Spring An opinionated guide
[Link] // @RestControllerAdvice
config/
[Link] // @Configuration
resources/
[Link] // database, server, logging config
test/
java/com/example/myapi/
[Link] // @SpringBootTest / @ExtendWith
■ Follow this structure from day one — Spring Boot's auto-configuration scans it.
— 14 —
Java for Spring An opinionated guide
SPRING BRIDGE — Everything Together — A Complete Spring Service
This is what a real Spring service looks like using every concept from this guide. Every line maps back to a layer above.
// LAYER 8: Lombok + LAYER 2: Immutable DTO with Builder
@Data @Builder @NoArgsConstructor @AllArgsConstructor
public class CreateUserRequest {
@NotBlank private String name;
@Email private String email;
}
// LAYER 1: Interface contract
public interface UserService {
UserDto createUser(CreateUserRequest req);
Optional<UserDto> findById(Long id); // LAYER 3: Optional
List<UserDto> findActiveAdmins(); // LAYER 5: List
}
// LAYER 8: Lombok | LAYER 1: Interface impl | LAYER 4: Exception
@Service
@RequiredArgsConstructor
@Slf4j
public class UserServiceImpl implements UserService {
private final UserRepository repo; // LAYER 1: Interface
private final PasswordEncoder encoder;
@Override
@Transactional
public UserDto createUser(CreateUserRequest req) {
[Link]("Creating user: {}", [Link]());
// LAYER 4: Domain exception
if ([Link]([Link]()))
throw new EmailAlreadyExistsException([Link]());
// LAYER 2: Builder pattern
User user = [Link]()
.name([Link]())
.email([Link]())
.password([Link]("temp"))
.role([Link]) // LAYER 5: Enum
.createdAt([Link]()) // LAYER 3: [Link]
.build();
return [Link]([Link](user));
}
@Override
public Optional<UserDto> findById(Long id) {
// LAYER 3: Stream + map on Optional
return [Link](id).map(UserMapper::toDto);
}
@Override
public List<UserDto> findActiveAdmins() {
// LAYER 3: Stream API | LAYER 5: filter + map + collect
return [Link]().stream()
.filter(u -> [Link]() && [Link]() == [Link])
.map(UserMapper::toDto)
.sorted([Link](UserDto::getName))
.collect([Link]());
}
}
— 15 —
Java for Spring An opinionated guide
SPRING BRIDGE — What to Learn Next — in this order
# Topic Why now Resource
ApplicationContext, @Bean,
1 Spring Core + DI [Link]/guides
@Component
@SpringBootApplication,
2 Spring Boot basics [Link]/quickstart
auto-config, YAML
@RestController,
3 Spring MVC / REST Spring in Action (book)
@RequestMapping, DTOs
@Entity, @Repository, JPQL,
4 Spring Data JPA Hibernate docs
pagination
Spring Security Filter chain,
5 Baeldung tutorials
(JWT) UserDetailsService, tokens
@SpringBootTest, MockMvc,
6 Testing JUnit 5 docs
@MockBean
@Aspect, @Around, cross-cutting
7 Spring AOP Spring docs
concerns
Spring Reactive
8 WebFlux, Mono, Flux Project Reactor docs
(opt.)
— 16 —