Keentune

Spring Boot curriculum

26 chapters
·
183 concepts
·
free
Everything the adaptive question bank can teach and test in Spring Boot, from foundations through advanced practice. Work through it in order, or start practising and let the questions find your level.
New here? Read the Spring Boot guide
A free 16-minute primer — the mental model, the mistakes beginners make, and what to practise first.
A. What Spring Boot is, and how an application starts
Boot is Spring Framework plus auto-configuration, starters and an executable jar, not a separate framework
@SpringBootApplication is @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan in one
scanning starts at the main class's own package, so a bean in a sibling package is never found
servlet, reactive or none is deduced from what is on the classpath, which is why adding a dependency changes startup
ApplicationRunner and CommandLineRunner execute after the context refreshes, in @Order sequence
B. The IoC container and bean definitions
the container constructs and wires collaborators; application code never calls new on one
ApplicationContext adds events, resource loading, AOP and eager singleton pre-instantiation
@Component, @Service, @Controller and @Repository register identically, but @Repository also translates persistence exceptions
inside @Configuration a @Bean method call is intercepted and returns the singleton; with proxyBeanMethods=false it is a plain call that builds a second object
ApplicationEventPublisher plus @EventListener decouples publisher from listener, and the delivery is synchronous unless made @Async
C. Dependency injection and wiring
mandatory collaborators become final fields and the object can never exist half-built
a class with exactly one constructor needs no @Autowired
it hides the dependency list, defeats final, and forces reflection or a context to test
resolution is by type first; the parameter or field name only breaks a tie between candidates
@Qualifier selects at one injection point, @Primary sets the default for all of them
injecting List<T> or Map<String,T> yields every bean of that type, ordered by @Order
ObjectProvider, Optional<T> or required=false for a collaborator that may legitimately be absent
a constructor cycle cannot be satisfied and fails at startup; since 2.6 even setter cycles are rejected unless spring.main.allow-circular-references is set
D. Bean scopes and lifecycle
one instance per container, shared across all threads, so mutable instance state is a race
a fresh instance on every lookup, and the container never calls its destruction callback
request, session and application scope, and the request context each one needs to exist
injecting a shorter-lived bean into a singleton needs proxyMode, or the first instance is captured forever
@PostConstruct/@PreDestroy, InitializingBean/DisposableBean, and @Bean(initMethod=...)
it wraps or replaces beans after instantiation, which is exactly how AOP proxies get substituted in
@Lazy and spring.main.lazy-initialization trade startup time for a failure that now surfaces on first use
E. Auto-configuration and how conditions resolve
auto-configuration classes are listed in META-INF/spring/…AutoConfiguration.imports, not discovered by component scanning
a configuration applies only when a type is present, tested by name so the missing class never has to load
the back-off rule: define the bean yourself and the auto-configured one silently withdraws
gating on a property's value, and what matchIfMissing changes
user configuration is always processed before auto-configuration, and @AutoConfiguration(before/after) orders the rest
turning one off with @SpringBootApplication(exclude=…) or spring.autoconfigure.exclude
--debug prints the positive and, more usefully, the negative matches that explain a missing bean
F. Starters, dependency management and the build
a spring-boot-starter-* artifact is a curated dependency aggregator containing no code of its own
the parent POM or the Gradle plugin's BOM pins compatible versions, so you declare dependencies without one
you override a managed version by redefining its property, not by pinning the dependency in place
the Maven/Gradle plugin rewrites the archive into an executable jar and provides bootRun
G. Externalized configuration and binding
command-line arguments beat environment variables, which beat a profile file, which beats application.properties
my.mainProject, my.main-project and MY_MAINPROJECT all bind to the same property
an environment variable is upper snake case with dots replaced by underscores, which is how container config reaches the app
@Value("${key:fallback}") injects a single property, with the colon supplying a default
type-safe binding of a whole prefix onto an object, with JSR-380 validation available
an immutable @ConfigurationProperties record, registered via @EnableConfigurationProperties or @ConfigurationPropertiesScan
a packaged password ships to every environment; config trees, mounted files and a secret store keep it out
H. Profiles and the Environment
spring.profiles.active must be set before the context starts; setting it from application code is already too late
application-{profile}.properties is loaded *in addition to* the base file and overrides only the keys it repeats
@Profile on a bean or configuration class, including the !prod negation form
spring.profiles.group activates a set of profiles under one convenient name
I. The servlet web layer
one front controller drives handler mapping, handler adapter, and result handling for every request
@RestController implies @ResponseBody, so a returned String is the body, not a view name
path, HTTP method, params, headers, consumes and produces each narrow which requests a handler claims
@PathVariable reads the URI template, @RequestParam reads the query string or form body
@RequestBody deserializes through an HttpMessageConverter chosen by the request's Content-Type
the response format is decided by the Accept header, the mapping's produces, and which converters are registered
full control of status, headers and body, versus a bare return value plus @ResponseStatus
controllers are singletons serving concurrent requests, so a mutable field is a cross-request data leak
a servlet Filter wraps every request including static resources; a HandlerInterceptor runs inside MVC and knows the handler
@CrossOrigin or a global CorsConfigurationSource, and the preflight OPTIONS request that must not be authenticated away
returning an ETag lets the client revalidate instead of refetching
a multipart file arrives as MultipartFile, not as a body the message converters read
a request carrying both needs each part bound separately
J. Validation and error handling
@Valid on a controller argument runs Bean Validation before the method body executes
@NotNull allows the empty string, @NotEmpty allows whitespace, @NotBlank allows neither
a BindingResult parameter must immediately follow the validated object, otherwise the framework throws instead of handing you the errors
@Validated on a class validates service method parameters, and it only works through the proxy
@ExceptionHandler handles within one controller; @RestControllerAdvice handles across all of them
RFC 9457 application/problem+json responses as the standard error shape instead of an ad-hoc map
K. Reactive: WebFlux versus MVC
MVC dedicates a thread per in-flight request; WebFlux multiplexes many requests over a small event loop
a Mono is zero-or-one and a Flux is zero-to-many, and neither does anything until something subscribes
a single blocking JDBC or block() call on an event-loop thread stalls unrelated requests and erases the benefit
the subscriber signals how much it can take, which is the guarantee a plain callback or future cannot give
spring.threads.virtual.enabled buys thread-per-request scalability on MVC without rewriting to a reactive pipeline
L. Calling other services
the current synchronous fluent HTTP client, the replacement for the deprecated RestTemplate
the non-blocking client, usable from MVC provided the caller does not immediately block on it
@HttpExchange declares the client as an interface and the framework generates the implementation
a client without connect and read timeouts turns one slow dependency into exhausted threads in your own service
M. Spring Data JPA and persistence
you declare an interface and Spring Data supplies the implementation at runtime; there is no class to write
findByLastNameAndAgeGreaterThan is parsed from the method name into a query, and a typo in the name fails at startup
@Query for JPQL, or nativeQuery=true for SQL, when the derived name would be unreadable or impossible
transient, managed, detached and removed, and what save actually does in each state
a managed entity modified inside a transaction is flushed at commit with no explicit save call
@ManyToOne and @OneToOne default to EAGER while collections default to LAZY, and both defaults surprise people
touching a lazy association after the persistence context closed, and how open-session-in-view hides the design problem
one query for the parents plus one per parent for the children; a join fetch or an @EntityGraph collapses it
Pageable and Sort, and that a Page costs an extra count query where a Slice does not
spring.jpa.hibernate.ddl-auto is a development convenience; Flyway or Liquibase is the production answer
Boot 4 sits on jakarta.persistence, so every javax.persistence import must move
a @Version column detects a concurrent write and fails the second one instead of losing it
@Lock takes a database lock for the duration, trading throughput for certainty
the side without mappedBy owns the foreign key; the other side is a mirror
the in-memory graph is only consistent if you set both sides yourself
cascade propagates operations; orphanRemoval deletes a child that is detached from its parent
IDENTITY forces an insert per row and defeats JDBC batching; SEQUENCE can pre-allocate
an @Embeddable has no identity of its own and maps into the owning table
subclasses share one table plus a discriminator, trading nullable columns for speed
a generated id is null before persist, so equals/hashCode built on it breaks in sets
the first level is per persistence context; the second is shared and needs invalidation
a bulk UPDATE or DELETE leaves stale managed entities unless the context is cleared
fetching only the fields a screen needs avoids loading whole entity graphs
Hibernate only batches when batch size is set and the id strategy allows it
the persistence context grows without bound in a long loop unless you clear it
Spring Data JDBC has no lazy loading, dirty checking, or persistence context
use plain SQL when the mapping cost of an ORM buys nothing
a pool too small queues requests; the default is HikariCP and it is usually right
routing reads to a replica needs an explicit routing DataSource, not a property
@CreatedDate and @LastModifiedDate need auditing switched on to populate at all
N. Transactions
the annotation works only because a proxy begins and commits around the call; it is not compiler magic
calling an annotated method from another method of the same class goes through this and skips the proxy entirely
a proxy cannot advise them, so the annotation is ignored with no warning
rollback happens on unchecked exceptions and Error; a checked exception commits unless rollbackFor says otherwise
the default joins the caller's transaction rather than opening a second one
it suspends the caller's transaction and borrows a second connection, which can deadlock a small pool
readOnly=true lets Hibernate skip dirty checking and lets routing send the work to a replica
an inner participating transaction that fails marks the whole thing rollback-only, so the outer commit throws
higher isolation removes more anomalies and costs more concurrency
NESTED uses a savepoint, so an inner failure need not roll the outer back
TransactionTemplate gives explicit boundaries where an annotation cannot reach
a transaction holds a connection, so a slow remote call holds the pool
work that must happen only if the commit succeeds belongs in a synchronization
O. AOP and proxying
the vocabulary: the aspect is the module, the advice is the code, the pointcut is where it applies
before, after returning, after throwing, after (finally), and around, which alone can suppress or replace the call
a bean with an interface can get a JDK dynamic proxy; a plain class gets a CGLIB subclass
final classes, final methods, private methods and static methods cannot be advised by a proxy
Boot proxies by class by default, so injecting the concrete type keeps working where interface-only proxies would fail
proxy-based, method-execution join points only, and only on container-managed beans
P. Spring Security
security is a chain of servlet filters running before the DispatcherServlet, which is why it can reject a request no controller ever sees
establishing identity versus checking permission, surfacing as 401 and 403 respectively
the component-based configuration that replaced WebSecurityConfigurerAdapter
rules are evaluated top to bottom and the first match wins, so a broad permitAll placed early opens everything below it
a PasswordEncoder with an adaptive hash, and the {id} prefix that lets stored hashes be migrated
@PreAuthorize and @PostAuthorize enforce at the service layer, through the same proxy mechanism as transactions
CSRF protection is needed for cookie-authenticated browsers, not for a stateless bearer-token API
the authenticated principal is held per thread, so it is absent on an @Async or executor thread unless propagated
Q. Testing
constructor injection means most services can be tested with new and no Spring context at all
loads the full application context, and webEnvironment decides whether a real server and port are started
@WebMvcTest, @DataJpaTest and @JsonTest load only the relevant slice, which is why a service bean is missing unless mocked
drives the whole MVC stack without a socket, so filters and serialization are covered but the container and network are not
@MockitoBean replaces a bean in the context, which a plain Mockito mock cannot do
contexts are cached and reused per unique configuration; every distinct configuration pays another startup
a @DataJpaTest rolls back each test, which also masks flush and constraint errors a real commit would surface
a real database in a container instead of H2, removing the dialect differences H2 quietly papers over
R. Actuator and observability
only health is web-exposed by default; everything else must be opted in explicitly
the aggregate status is composed from per-component indicators, and a custom one plugs into the same aggregation
separate probes, because a failed readiness check should remove traffic while a failed liveness check restarts the process
a vendor-neutral metrics facade where tags are dimensions and an unbounded tag value is a cardinality explosion
one Observation emits both a metric and a trace span, so instrumentation is written once
actuator reveals configuration, beans and environment, so it must be authorized or bound to a separate management port
S. Packaging, images and native builds
dependencies stay as nested jars under BOOT-INF/lib with a custom loader, so it is not a shaded uber-jar
deploying to an external servlet container requires SpringBootServletInitializer and a provided-scope embedded server
layers ordered by change frequency so a rebuilt image reuses the unchanged dependency layer
bootBuildImage produces an OCI image with no hand-written Dockerfile
ahead-of-time compilation gives millisecond startup and low memory, paid for in build time and lost dynamism
reflection, dynamic proxies and resources must be registered at build time, which is why classpath tricks break natively
AOT fixes bean definitions and condition outcomes at build time, so profiles can no longer change which beans exist
T. Asynchronous and scheduled work
@Async returns immediately only through the proxy, so a self-invoked async method runs synchronously
a void @Async method swallows its exception; returning CompletableFuture carries the failure back to the caller
fixed rate measures start to start, fixed delay measures end to start
the default scheduler runs one thread, so one slow job delays every other scheduled job
every replica fires the same cron expression unless a lock or leader election makes exactly one of them act
U. Logging and running in production
application code logs through SLF4J while Boot configures Logback behind it, so the binding can change without touching code
emitting JSON lines for a log aggregator instead of a human-formatted line something has to re-parse
server.shutdown=graceful finishes in-flight requests before the context closes, which is what makes a rolling deploy invisible
automatic restart and live reload are development-only and are excluded from the repackaged archive
V. Spring Boot 4 and Framework 7: what changed
Boot 4 raises the minimum Java version, which is the first thing to check before upgrading
null-safety annotations standardise on JSpecify, replacing the older Spring annotations
the JSON stack moved, so custom serializers and modules need re-checking
the single autoconfigure jar was split into modules, changing what a starter drags in
CDS is auto-configured and cuts startup time by sharing a pre-parsed class archive
@ConcurrencyLimit caps in-flight invocations without a thread pool of your own
the docker-compose module starts declared services for local development only
Boot 4.1 auto-configures gRPC servers, in more than one server mode
observability exports can be switched off with a single property
deferring the datasource connection changes when a pool slot is taken
filtering resolved addresses on HTTP clients is a defence against SSRF to internal hosts
Redis pub/sub listeners need a listener container defined, or nothing is delivered
W. Resilience: retry, recovery and circuit breaking
@Retryable makes retry a declaration rather than a loop, and is built in from Framework 7
@Recover is the fallback that runs once the retries are used up
a breaker stops calling a failing dependency for a cool-down instead of queueing behind it
X. Messaging and asynchronous integration
@KafkaListener and @RabbitListener bind a method to a broker destination
at-least-once delivery means a listener must be safe to run twice on the same message
Y. Caching
@Cacheable works through a proxy, so a self-invocation bypasses it entirely
Z. Distributed systems: config, discovery and gateways
a config server externalises configuration so instances do not each carry their own
discovery replaces hard-coded hosts with a registry instances publish to
a gateway gives one entry point for routing, cross-cutting concerns and rate limits
Keentune is not affiliated with or endorsed by the organizations whose documentation informs these maps.
All about Spring Boot practice
Also on your phone
All exam, test, and product names and trademarks are the property of their respective owners and are used here for identification and reference only. Keentune is independent study practice — not affiliated with, authorized, or endorsed by any of these organizations.
© 2026 SportaApp LLC