Everything the adaptive question bank can teach and test in Spring Boot, from foundations through advanced practice. Work through it in order, or start practicing and let the questions find your level.
A. What Spring Boot is, and how an application starts
Everything it adds is a default you can override, so nothing is hidden that cannot be inspected or replaced. Treating it as a separate framework leads people to look for its documentation instead of the underlying one.
Knowing the three parts matters because each can be needed separately — a test may want the configuration without the scanning, and disabling auto-configuration is a property of one of them.
Placement of the main class silently defines the scanned region, so a bean that is never found is usually in the wrong package rather than wrongly annotated. Moving the class moves the boundary.
Startup behavior is deduced from what is present, which is why adding an unrelated dependency can change how the application starts. The classpath is an input, not just a list.
These run after the context is ready, so they can use any bean, and ordering between them is explicit rather than incidental. Startup work belongs here rather than in a constructor.
B. The IoC container and bean definitions
Because construction is external, a collaborator can be swapped without touching the class that uses it — which is what makes the code testable and configurable at the same time.
The richer interface is what almost everything assumes, and eager singleton creation is why a misconfiguration surfaces at startup rather than on first request. That timing is a feature.
They differ in intent and in one behavior: the persistence one translates vendor exceptions into a common hierarchy. Otherwise the choice is documentation for the reader.
Method interception is what makes a call between bean methods return the singleton. Turning it off is a startup optimization that quietly changes those calls into ordinary ones.
Publisher and listener need not know each other, and delivery is synchronous by default — so a slow listener slows the publisher unless it is made asynchronous.
C. Dependency injection and wiring
A fully constructed object cannot exist in a partial state, and the dependency list is visible in the signature. Both properties come from the same choice.
With one constructor the container has no ambiguity to resolve, so the annotation adds nothing. Its absence is not a sign injection was forgotten.
It hides what the class needs, prevents final fields, and makes the class untestable without reflection or a running container. The convenience is paid for at every test.
Type is the primary key and the name only breaks a tie, so renaming a parameter can change which bean is injected when several candidates exist.
One decides a single injection point and the other sets a default everywhere. Reaching for the wrong one produces a fix that works in one place and not the next.
Requesting a collection yields every candidate, which is how plugin-style designs work. Ordering is explicit rather than classpath-dependent.
Where a collaborator may genuinely be absent, declaring that is better than a null check, because it states the intent and keeps startup from failing.
A constructor cycle is unsatisfiable by construction, so it fails at startup rather than later. The setter form is now also rejected by default, which turns a hidden design problem into a visible one.
D. Bean scopes and lifecycle
One shared instance serving concurrent requests means any mutable field is shared state. This is the most common source of data crossing between users.
A new instance each time, and the container stops tracking it afterwards — so destruction callbacks never run and cleanup is the caller's responsibility.
These require a request context to exist, so using one outside a request, such as on a background thread, fails rather than falling back.
A singleton injected with a shorter-lived bean captures the first instance forever unless a proxy resolves it per call. The proxy is what makes the scope meaningful.
Several mechanisms do the same job, which matters when mixing library and application code. Work needing injected collaborators belongs after construction, not inside it.
It can wrap or replace a bean after instantiation, which is exactly how proxies are substituted. Understanding it explains why the injected object is not always the class you wrote.
Deferring creation shortens startup and moves failures to first use, which is the wrong trade for a production service and often the right one for a test.
E. Auto-configuration and how conditions resolve
Auto-configuration is registered by a listed file rather than found by scanning, which is why it works for classes outside your packages and why the list is the place to look.
The test is by class name, so the absent type never needs to load. That is what allows a configuration to reference something that may not be on the classpath.
Defining the bean yourself causes the provided one to withdraw silently, which is the entire override mechanism. It also explains a bean appearing when you delete yours.
Gating on a value makes a feature switchable by configuration, and whether it applies when the property is absent is a separate decision that changes the default.
User configuration is processed first, which is what makes the back-off rule work. Ordering between auto-configurations is declared rather than alphabetical.
Turning one off is available both in code and as a property, and the property form is what lets an environment differ without a rebuild.
The negative matches explain why a bean you expected is missing, which is the question that actually gets asked. Reading them is faster than guessing at the classpath.
F. Starters, dependency management and the build
It contains no code, only a curated set of dependencies, so adding one is a statement about what you need rather than an import of behavior.
Versions are pinned centrally so the combination is known to work together. Declaring a dependency without a version is the point rather than an omission.
Overriding through the managed property keeps the rest of the set consistent, where pinning one dependency directly can leave it mismatched with its siblings.
The plugin produces the runnable archive, so the build output differs from an ordinary library jar. Running the application and building it are the same plugin's concern.
G. Externalized configuration and binding
Later sources override earlier ones in a defined order, which is what allows a container to override a packaged file without rebuilding. Knowing the order settles most surprises.
Several spellings bind to one property, which is what lets an environment variable and a properties file express the same setting. It also means a typo can silently miss.
The transformation from dotted key to upper snake case is mechanical, and it is how configuration reaches a containerized application without a file.
It injects one property with an optional fallback, which is convenient for a single value and becomes unmanageable across a group. That is what the typed alternative exists for.
Binding a whole prefix onto an object gives type safety and a place to validate, so a misconfiguration fails at startup rather than at use.
An immutable configuration object cannot be modified after binding, and it must be registered explicitly so the container knows to bind it.
A packaged credential ships to every environment that receives the artifact and cannot be rotated without a rebuild. External sources solve both problems at once.
H. Profiles and the Environment
Activation must happen before the context starts, so setting it from application code is already too late. It belongs in the environment or the command line.
The profile file is loaded in addition to the base, overriding only the keys it repeats — so a key absent from it keeps the base value rather than becoming unset.
It conditions a bean on the active set, and the negation form is what expresses everything except one environment. Beans are then absent rather than disabled.
Grouping lets one name activate several profiles at once, which keeps the environment variable simple as the set grows. It also means the grouping is declared in configuration rather than remembered by whoever deploys.
I. The servlet web layer
A single entry point handles mapping, invocation and result handling for every request, which is why cross-cutting behavior can be added in one place.
The first implies the response body annotation, so a returned string is content. With the second it is a view name, which is the source of a confusing class of errors.
Each attribute narrows which requests a handler claims, so two handlers can share a path and differ by method or content type. Ambiguity is reported at startup.
One reads the URI template and the other the query or form, so the choice follows from where the value is in the request rather than from preference.
Deserialization is chosen by the request's content type, so an unexpected type produces a converter failure rather than a parsing error in your code.
The response format results from the client's preference, the mapping's declaration and what converters exist. A missing converter presents as an unacceptable-type response.
Returning it gives full control of status, headers and body in one place, where the annotation form fixes the status for every outcome of the method.
Controllers are shared singletons handling concurrent requests, so a mutable field can leak one user's data into another's response. It is the same hazard as any shared bean.
One runs outside the framework and sees everything including static resources; the other runs inside and knows which handler was selected. The choice follows from what you need to see.
The browser sends an unauthenticated preflight request, so a security rule that requires authentication on every path breaks cross-origin calls before they start.
Returning a validator lets the client revalidate rather than refetch, so the saving is the response body — which is where most of the bytes are.
A multipart part is not read by the message converters, so it binds to a file abstraction instead of a body object. Expecting the usual binding is the common mistake.
A request carrying both a document and a file needs each part bound separately, because no single converter covers the whole body.
J. Validation and error handling
Validation runs before the method body, so an invalid request never reaches your logic. Omitting it means the constraints on the type are declared and never enforced.
The three differ precisely in what they permit — an empty value, whitespace, or neither. Choosing the loosest by habit lets blank input through a field that looks validated.
It must immediately follow the object it reports on, because the binding is positional. Placed elsewhere the framework throws instead of handing you the errors, which reads as a validation bug.
Validating service parameters works through the proxy, so a self-invoked call is unvalidated. It is the same proxy limitation that affects transactions and caching.
One handles within a controller and the other across all of them, so the choice is about scope. A cross-cutting error shape belongs in the global form.
A standard error body means clients can parse failures uniformly instead of learning your ad-hoc shape. It is a contract decision rather than a formatting one.
K. Reactive: WebFlux versus MVC
One dedicates a thread per in-flight request and the other multiplexes many over few threads. The second scales with connections rather than with threads, at the cost of a different programming model.
Nothing happens until subscription, so building a pipeline and never subscribing produces no work and no error. It is the commonest first surprise.
One blocking call occupies an event-loop thread that was serving many requests, so unrelated requests stall. The benefit of the model is erased by a single such call.
The subscriber signals how much it can accept, which bounds memory when a producer is faster than a consumer. Plain callbacks and futures have no way to express that.
Enabling them buys much of the scalability without rewriting to a reactive pipeline, which makes the reactive rewrite a choice rather than the only route.
L. Calling other services
The current synchronous client, replacing the older one, so new code should not start from the deprecated API. The fluent shape is closer to the reactive client's.
Non-blocking and usable from a servlet application, provided the caller does not immediately block on the result — which would reintroduce the thread cost it exists to avoid.
Declaring the client as an interface moves the HTTP details to annotations and removes the hand-written implementation, which is one fewer thing to test.
Without connect and read timeouts, a slow dependency holds your threads until it responds, so its outage becomes yours. Defaults are frequently absent or generous.
M. Spring Data JPA and persistence
You declare the interface and the implementation is supplied at runtime, so there is no class to write or test. What you are reviewing is a declaration rather than code.
The method name is parsed into a query, so a typo fails at startup rather than at runtime. That early failure is the main benefit over a string query.
An explicit query is the escape hatch when a derived name would be unreadable or cannot express the query. The native form trades portability for capability.
What a save call does depends on the state the object is in, which is why the same call can insert, merge or do nothing. State is the missing variable in most confusion here.
A modified managed entity is written at commit without an explicit save, which surprises people in both directions — unintended writes, and expected writes that never happened.
The defaults differ by association type and neither matches what most people assume, so relying on them produces either extra queries or failures on access.
Touching a lazy association after the context closed fails, and keeping the context open for the whole request hides the design question rather than answering it.
One query for parents and another per parent is easy to write and invisible until the data grows. A fetch join or an entity graph collapses it into one.
A page costs an additional count query to report totals; a slice does not because it only reports whether more exist. Choose by whether the total is actually displayed.
Automatic schema generation is a development convenience that can drop data, so production schema changes belong in a versioned migration tool.
The package namespace moved, so every import must change. It is mechanical and it is a hard break rather than a deprecation.
A version column makes the second concurrent write fail rather than silently overwrite the first. The failure is the feature.
Taking a database lock guarantees exclusivity for the transaction's duration, at the cost of blocking others. It trades throughput for certainty.
The side without the mapping attribute owns the foreign key, so changes on the mirror side are not persisted. This is why a set on one side alone can appear not to save.
The database has one foreign key while the object graph has two references, and only you maintain the second. An inconsistent graph produces confusing reads before any flush.
Cascading propagates an operation to associated entities; orphan removal deletes a child that has been detached. They are different mechanisms and are routinely conflated.
An identity column requires an insert per row to obtain the value, which prevents batching. A sequence can pre-allocate, so the strategy decides whether batching is possible at all.
A value type has no identity and maps into the owner's table, so it is a way to group columns rather than a separate entity. Equality is by value.
One table for the hierarchy avoids joins and requires nullable columns for subclass fields. It trades schema tidiness for read speed.
A generated identifier is null until persist, so an entity placed in a set before saving changes its hash afterwards. Business keys avoid the problem entirely.
The first level is scoped to one persistence context and needs no invalidation; the shared one does, and stale entries there are a correctness problem rather than a performance one.
A bulk statement goes straight to the database, leaving managed copies stale. Clearing the context afterwards is what keeps subsequent reads correct.
Fetching only the fields a screen needs avoids loading whole graphs, which is usually a larger win than any caching added afterwards.
Batching is off unless a size is configured, and the identifier strategy must allow it. Both conditions must hold, which is why enabling one alone changes nothing.
The persistence context accumulates every entity it has seen, so a long loop grows memory until it fails. Periodic clearing is what bounds it.
Without lazy loading, dirty checking or a persistence context, behavior is simpler and more predictable — and features people rely on are absent by design rather than missing.
Where the mapping an ORM provides buys nothing, plain SQL is less machinery and more control. The choice is per use case rather than per application.
A pool smaller than concurrent demand queues requests, and a much larger one moves the contention to the database. The default is a reasonable starting point rather than a problem to fix.
Sending reads elsewhere requires a routing datasource that decides per transaction; it is not something a property enables. The read-only flag is what the routing keys on.
The annotations populate nothing unless auditing is enabled, so the fields stay null and it looks like a mapping problem rather than a missing switch.
N. Transactions
A proxy opens and commits around the call, so everything about the annotation follows from that — including every situation where it appears to be ignored.
An internal call goes directly through the object reference and never reaches the proxy, so no transaction starts. The annotation is present and does nothing.
A proxy cannot intercept them, so the annotation is silently inert. There is no warning, which is why this is found by debugging rather than by reading.
Unchecked exceptions roll back and checked ones commit, which is the opposite of what most people assume. Declaring the rule explicitly removes the guess.
Joining the caller's transaction is the default, so a nested service call shares one boundary. That is usually what you want and is worth knowing rather than assuming.
Suspending the outer transaction and taking a second connection can exhaust a small pool, because one logical call now holds two. It is the cost people do not price.
It lets the persistence layer skip dirty checking and lets routing send work to a replica, so it is both a performance and an infrastructure signal.
A failed inner participant marks the shared transaction for rollback, so the outer commit fails even though the exception was caught. The catch did not undo the marking.
Each level removes more anomalies and permits less concurrency, so the choice is a trade rather than a correctness setting with a best value.
A savepoint lets an inner failure be undone without abandoning the outer transaction, which is different from a separate transaction and depends on database support.
Explicit boundaries reach where an annotation cannot — inside a lambda, around part of a method, or where the proxy is bypassed.
A transaction holds a connection for its duration, so a slow remote call holds a pool slot while doing nothing with the database. The pool exhausts under load.
Work that must not happen if the commit fails — publishing an event, sending a message — belongs in a callback tied to the commit rather than at the end of the method.
O. AOP and proxying
Three separate things: the module, the code that runs, and the expression selecting where. Most confusion here is vocabulary rather than mechanism.
They differ in when they run and what they can do — only the surrounding form can suppress the call, change the arguments or replace the result.
Interface-based and subclass-based proxying have different limitations, so which is used determines what can be injected and what can be advised.
Anything a proxy cannot intercept is silently unadvised — final classes and methods, private methods, static methods. This one fact explains most of the transaction and caching surprises.
Proxying by class by default means injecting the concrete type keeps working, which is why code that would fail under interface-only proxying does not here.
Only method executions on container-managed beans are eligible, so field access, constructors and plain objects are out of scope. The limitation follows from the proxy approach.
P. Spring Security
Security runs before the dispatcher, so a request can be rejected without any controller being involved. That is why a breakpoint in a controller never hits on a 401.
Establishing who you are and checking what you may do are separate steps producing different responses. Confusing them makes an authorization bug look like a login bug.
Configuration is a bean rather than an extended base class, which makes multiple chains for different path groups a natural rather than awkward arrangement.
First match wins, so a broad permissive rule placed early makes every rule below it unreachable. Ordering is the configuration, not a stylistic arrangement.
An adaptive hash is deliberately slow, and the stored prefix identifies the algorithm so hashes can be migrated without invalidating existing passwords.
Enforcement at the service layer runs through the same proxy mechanism as transactions, so it carries the same self-invocation limitation.
The protection addresses browsers automatically attaching cookies, so it is needed for cookie sessions and not for a bearer token sent deliberately. Disabling it for a cookie app is the real mistake.
The principal is held per thread, so it is absent on an asynchronous or pooled thread unless propagation is configured. The call appears unauthenticated.
Q. Testing
Most services can be constructed directly and tested without a context, which is fast and precise. Reaching for a context first is the commonest reason test suites become slow.
It loads the full context, and the web environment setting decides whether a real server and port exist — which determines whether you can use a real HTTP client.
A slice loads only part of the context, so beans outside it are absent and must be mocked. That absence is the point rather than a misconfiguration.
It exercises the MVC stack without a socket, so filters and serialization are covered while the container and network are not. Knowing the boundary tells you what the test proves.
Replacing a bean in the context is different from creating a mock locally, because the substitution has to reach whatever the container injected.
Contexts are cached per unique configuration, so each distinct configuration adds a startup. Small gratuitous differences between tests multiply the suite's runtime.
Rolling back each test keeps them isolated and also prevents the flush that would reveal constraint violations, so some failures only appear in production.
A real database removes the dialect and behavior differences that an in-memory substitute papers over, which is where a class of bugs hides until deployment.
R. Actuator and observability
Almost nothing is exposed by default, which is the safe starting point. Anything you can reach without configuring it is deliberate.
The overall status is composed from components, so one failing dependency can mark the application down. Whether it should is a design decision.
These answer different questions — remove traffic, or restart the process — so wiring the same check to both turns a temporary dependency failure into a restart loop.
Tags are dimensions, and each distinct combination is another series. An unbounded tag value is how a metrics bill and a metrics backend both fall over.
One instrumentation point emits both a metric and a trace span, so the two stay consistent and are written once rather than twice.
These endpoints expose configuration, beans and environment, so leaving them open discloses far more than a health check. A separate port or authorization is required.
S. Packaging, images and native builds
Dependencies remain as nested archives with a custom loader rather than being flattened, which avoids the file collisions a shaded archive produces.
Deploying into an external container requires an initializer and the embedded server marked provided, or two servers end up on the classpath.
Ordering layers by how often they change means a rebuild reuses the unchanged dependency layer, which is where almost all of the image size is.
An image is produced without a hand-written container file, which removes a file that is easy to get subtly wrong and hard to keep current.
Startup and memory improve substantially, paid for in build time and in losing runtime dynamism. It is a deployment-shape decision rather than an optimization.
Anything resolved at runtime must be declared at build time, which is why classpath scanning and dynamic proxies need explicit registration to work natively.
Bean definitions and conditions are fixed at build time, so profiles can no longer decide which beans exist. Configuration that changes structure must move earlier.
T. Asynchronous and scheduled work
Asynchrony comes from the proxy, so a self-invoked call runs on the caller's thread with no indication anything is wrong. Same limitation as transactions and caching.
A void asynchronous method has nowhere to report a failure, so the exception is lost. Returning a future carries the failure back to whoever waits on it.
One measures between starts and the other between end and next start, so under a slow task the first can queue executions while the second cannot.
The default scheduler is single-threaded, so one long job delays every other scheduled job. This presents as unrelated jobs mysteriously running late.
Every replica runs the same schedule, so a job intended to happen once happens per instance. A lock or leader election is what makes it exactly once.
U. Logging and running in production
Application code depends on the facade rather than the implementation, so the backing library can change without code changes. It is why two logging configurations can coexist.
Emitting fields rather than a formatted sentence means an aggregator can query them without parsing, which is what makes logs searchable at scale.
Finishing in-flight requests before closing is what makes a rolling deploy invisible to users. Without it, every deployment drops whatever was in progress.
Restart and reload are development aids and are excluded from the packaged archive, so behavior differs between running locally and running the artifact.
V. Spring Boot 4 and Framework 7: what changed
The minimum Java version rises, so the runtime must move before anything else can. It is the first compatibility check rather than one of many.
Null-safety annotations standardize on a common specification, which improves tooling agreement at the cost of updating the older annotations.
The JSON stack moved, so custom serializers and registered modules need re-checking rather than assuming they carry over unchanged.
Splitting the single jar into modules changes what each starter brings in, so a class previously present by accident may now be absent.
Sharing a pre-parsed class archive cuts startup without changing the application, so it is a deployment-level improvement rather than a code one.
Capping in-flight invocations declaratively bounds load without managing a thread pool, which makes it a back-pressure control rather than a scheduling one.
Declared services start for local development only, which removes the manual step of running dependencies before the application.
Server support is auto-configured in more than one mode, so the choice of mode is a deployment decision rather than a library one.
Exports can be switched off with a single property, which matters for environments where the collector is absent and the failures would otherwise be noisy.
Deferring the connection changes when a pool slot is taken, which can substantially reduce contention for transactions that do not always touch the database.
Filtering resolved addresses blocks a client from reaching internal hosts, which is the defense against a request being redirected inward by an attacker.
Publish-subscribe listeners need a container defined or nothing is delivered, and the silence looks like a broker problem rather than a missing bean.
W. Resilience: retry and concurrency limits in Framework 7
Declaring the policy keeps it visible and separate from the logic. Count attempts carefully: maxRetries = 3 allows up to four calls, because the first call is not a retry.
The annotations are only metadata. Without @EnableResilientMethods on a configuration class, the method runs once and nothing reports that the retry was skipped. A call from inside the same bean skips the proxy, so it is never retried either.
The framework rethrows the last exception the method threw. The @Recover fallback method belonged to Spring Retry, a separate library that is now archived, so in Framework 7 a fallback is a catch in the caller.
Retrying every exception repeats failures that can never succeed, such as a validation error. Name the transient types in includes or rule out permanent ones in excludes. A custom predicate runs after both.
The default multiplier of 1.0 keeps every wait at delay. Raise it for exponential backoff, cap the growth with maxDelay, and add jitter so that clients which failed together do not all retry at the same instant.
You build a RetryPolicy with the same settings and pass a lambda. invoke rethrows the last unchecked exception, while execute throws a checked RetryException that holds the exception from every attempt.
The order holds whatever order you set on @EnableResilientMethods, so a failed attempt rolls back before the next one starts. @Async is different: it wraps retry by default, so every attempt runs on the same async thread.
A limit above 1 throttles like a pool size. Over the limit, callers wait by default, and the REJECT policy throws an exception instead. On a class, every method shares one limit.
Retry keeps calling, while a breaker stops. Framework 7 ships retry and concurrency limits only, and Spring Retry's @CircuitBreaker went with that archived library. A breaker comes from Spring Cloud Circuit Breaker, for example over Resilience4j.
X. Messaging and asynchronous integration
Binding a method to a destination removes the polling loop and the acknowledgment handling, leaving the message-processing logic as the only code you write.
Delivery guarantees permit duplicates, so processing must be safe to repeat. Designing for it is cheaper than trying to achieve exactly-once delivery.
Y. Caching — the abstraction
Caching comes from the proxy, so a self-invocation computes every time with no indication the cache was skipped. The third instance of the same limitation.
Three annotations doing three different things — skipping the call, always calling and storing, and removing. Using the wrong one produces a cache that never updates or never hits.
One is evaluated before the call and the other after, which is why only the second can inspect the result. The names do not convey the ordering.
Locking the entry means one thread computes while others wait, instead of every caller recomputing on a miss. It is the fix for a stampede on an expensive value.
Parameters form the key by default, which is wrong whenever an argument is not part of the identity. An expression or a custom generator states the intent.
Whether the eviction survives a thrown exception depends on when it runs, so the setting decides whether a failure leaves a stale entry behind.
For supported wrapper types the result expression sees the unwrapped value, so a condition written against the wrapper does not behave as expected.
AA. Cache providers and auto-configuration
The first provider present on the classpath wins unless one is named explicitly, so adding a library can change which cache is in use without any configuration change.
With no caching library present the store is an in-memory map, which works locally and does not survive a restart or span instances. It is why a cache can look correct and do nothing in production.
The abstraction has no notion of expiry, so a time-to-live is configured per provider and per cache. Expecting a portable setting is the misunderstanding.
Tuning the auto-configured manager keeps the auto-configuration in place, where defining your own replaces it and loses whatever else it was doing.
Z. Distributed systems: config, discovery and gateways
Externalizing configuration means instances do not each carry their own copy, so a change is made once. It also adds a dependency that must be available at startup.
A registry replaces hard-coded hosts, which is what allows instances to move and scale without redeploying their callers.
One entry point is where routing, cross-cutting concerns and rate limits can be applied consistently, rather than being repeated in every service.
Keentune is not affiliated with or endorsed by the organizations whose documentation informs these maps.
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.