Experienced interview questions
Auto-configuration inspects the classpath and existing beans, then conditionally configures beans using @Conditional annotations. It is driven by entries in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports.
At startup, Spring Boot scans the classpath to see which libraries are present and looks at the beans that already exist in the application context. It then evaluates a large set of configuration classes, each guarded by conditional annotations such as @ConditionalOnClass, which only activates if a particular library is on the classpath, and @ConditionalOnMissingBean, which only activates if no matching bean has already been defined.
This design means auto-configuration is additive and overridable rather than fixed. Adding the H2 database dependency, for instance, is enough to switch on an in-memory database connection automatically, because the conditional checks pass once that library appears. Defining a DataSource bean explicitly makes the conditional configuration back off and use that one instead. Understanding this pattern helps when debugging unexpected behaviour, since it can usually be traced back to a condition either matching or failing to match.
The standard approach is a single class annotated with @RestControllerAdvice that contains one or more methods annotated with @ExceptionHandler, each mapped to a specific exception type. When any controller in the application throws a matching exception, Spring routes it to the appropriate handler method instead of letting the default error page or a raw stack trace reach the caller.
Centralising error handling this way keeps individual controllers focused purely on their main job, since they no longer need repeated try-catch blocks scattered everywhere. It also guarantees every error response has the same shape, typically a small JSON object with a message and an appropriate HTTP status code, which makes life much easier for whoever is consuming the API, whether that is a front-end team or an external partner, because they can write one consistent way of handling failures instead of guessing at each endpoint.
The first step is finding the actual slow query rather than guessing, usually by enabling SQL logging or a profiler and looking for a pattern where loading a list triggers one extra query per item, known as the N plus one problem. Fixing that specific issue with a fetch join or an entity graph, so related data loads in a single query instead of many, often produces the single largest improvement.
Beyond that one fix, a few standard techniques usually round out the effort: add a database index on columns filtered or sorted on often, since without one the database scans every row; use a projection to select only the fields the screen actually needs instead of loading whole entities; and always page large result sets with a size limit rather than loading everything into memory at once. Applied together, these changes typically turn a query that takes seconds into one that takes milliseconds.
Dependency injection means a class receives the objects it depends on from the outside, usually through its constructor, rather than creating those objects itself with the new keyword. In a Spring application, the framework builds these dependency objects, called beans, once and hands them to whichever class asks for them, wiring the whole application together automatically based on the types involved.
The practical benefit shows up most clearly in testing: because a class accepts its dependencies as constructor parameters, a test can pass in a fake or mock version instead of the real one, allowing the class to be tested in isolation without a real database or network call. It also makes the codebase easier to change over time, since swapping one implementation for another, say switching from one payment provider to another, means changing what gets injected rather than editing every class that uses it.