Loading...
Loading...
Backend interview guide
Dependency injection, auto-configuration, beans, REST controllers and configuration. Every answer is short enough to say aloud, and every question links to the lesson that teaches it.
60 questionsLast updated
A client app should not have to parse an English sentence to find numbers. With fields, the handler can add them to the error body, and the app can show "only 8 left" or lower the quantity for the user.
Lesson: Custom exceptions such as NotFound and ConflictIt picks the one whose declared type is closest to the thrown exception in the class hierarchy. Handlers in the controller itself are tried before advice handlers. The order of methods in the file does not matter.
Lesson: @RestControllerAdvice and @ExceptionHandlerLog the method, path and full stack trace once, and return a 500 with a fixed, harmless sentence. It must not return the exception message, and it must not turn Spring MVC's own errors such as 404 and 405 into 500. Extending ResponseEntityExceptionHandler or checking ErrorResponse keeps those.
That the class has @RestControllerAdvice and sits under the main application package so scanning finds it, that the method has @ExceptionHandler with the right exception type, and that no controller-local handler or try/catch handles the exception first.
The first comes from @Valid on a request body or model object. The second comes from method validation in Spring 6.1+, when constraints sit directly on controller parameters such as @Positive long id. If a method has both, the body errors also arrive inside HandlerMethodValidationException as ParameterErrors.
No. Bean Validation already checks every constraint, so I return all of them in one reply. The user fixes the whole form at once instead of finding problems one round trip at a time.
Lesson: Validation error responses with field detailsIgnoring them hides client bugs, such as a typo in stok that silently leaves stock at 0, or a client sending id and thinking it chose the id. The ShopLite contract makes unknown fields a 400, which needs fail-on-unknown-properties=true because Spring Boot ignores them by default.
From an @ExceptionHandler method in a @RestControllerAdvice, I build it with ProblemDetail.forStatusAndDetail, set type and title, add extensions with setProperty, and return it. Spring sets the content type and fills instance with the request path.
type is a stable identifier, a URI, for the kind of problem; title is a human summary of it and may be reworded or translated. Clients should branch on type and show title or detail to people.
It suits a small web-only case, because the exception carries its own status and body and needs no handler. ShopLite's services should not depend on HTTP classes, and keeping the exception-to-problem mapping in one handler keeps the contract in one place.
Lesson: ProblemDetail and RFC 9457 errorsSpring's class for RFC 9457 problem details: type, title, status, detail, instance plus extensions, sent as application/problem+json. It is a standard, so clients and tools understand it, and Spring's own exceptions already produce it.
409 when the request clashes with the current state: a duplicate, not enough stock, an order in the wrong status, a resource still in use. 422 when the request is valid but refers to data that breaks a rule, such as an unknown category id in the body.
Lesson: Practice: exception handlingThe log shows HttpMessageNotReadableException reaching the catch-all. I add a handler, or override handleHttpMessageNotReadable in a ResponseEntityExceptionHandler subclass, that returns 400 malformed-request with a safe sentence.
Each place is a property source with a fixed priority. Command-line arguments win, then Java system properties, then OS environment variables, then config files outside the jar, then files inside the jar. That is why the jar can ship defaults and the server can override them without a rebuild.
Lesson: application.properties vs application.ymlReplace the dots with underscores, remove the dashes and write it in upper case: shoplite.max-page-size becomes SHOPLITE_MAXPAGESIZE. A list index like shoplite.cities[0] becomes SHOPLITE_CITIES_0.
YAML guesses types: 1.10 becomes the number 1.1, 0123 an octal number and off, no and NO become false. The app then reads a different value than the one written. Quoting the value keeps it as text.
It takes the prefix from @ConfigurationProperties("shoplite") and the component name, matched with relaxed binding. So lowStockThreshold is filled from shoplite.low-stock-threshold, SHOPLITE_LOWSTOCKTHRESHOLD or any other spelling of that key.
The field is set after the constructor runs, so code in the constructor sees 0 or null. The field also cannot be final, and a unit test has to use reflection to set it. A constructor parameter avoids all three problems.
Because the service takes the record in its constructor, the test just creates one with new ShopLiteProperties(5, 20, 100, ...) and passes it in. No Spring context, no properties file and no reflection are needed.
Usually with the environment variable SPRING_PROFILES_ACTIVE=prod on the server or in the container. A --spring.profiles.active=prod argument also works. After deploying, I check the start-up log line that lists the active profiles.
I define an interface, mark the fake implementation @Profile("!prod") and the real one @Profile("prod"). Services depend only on the interface. Using !prod instead of dev makes sure tests and runs without a profile also get the fake.
I put @ActiveProfiles("test") on the test class and keep application-test.properties in src/test/resources. The test context then loads the test settings and the beans meant for that profile.
SPRING_DATASOURCE_PASSWORD maps straight to spring.datasource.password by relaxed binding, so no line in the file is needed. DB_PASSWORD is just a variable name that a placeholder such as ${DB_PASSWORD} refers to. Both work; short names are easier to share and read.
No. The old commit still holds the secret and may already be cloned or scanned. I rotate the secret first, then untrack the file, add it to .gitignore, and if needed rewrite the history with git filter-repo and force-push.
SLF4J is a logging facade: the API my code calls. Logback is an implementation behind it that formats and writes the lines, and it is Spring Boot's default. Because the code depends only on SLF4J, the implementation can be swapped (for example to Log4j 2) without changing any class.
Lesson: Logging with SLF4J: levels and what to logI pass the exception object as the last argument: log.error("Payment failed for order {}", orderId, e). That prints the message plus the full stack trace. I log it once, in the place that handles it, not at every layer it passes through.
Passwords, OTPs, tokens and API keys, full card numbers and CVVs, and full personal identifiers such as phone, Aadhaar or PAN numbers. Logs are copied widely and kept for a long time, so I log ids instead, and mask personal values when they are needed.
Lesson: Logging with SLF4J: levels and what to logA Boot module that adds endpoints for operating a running app: health, info, metrics, loggers, environment and more. I use /actuator/health for load balancers and deployment tools, and /actuator/info to see which version is running.
Liveness says whether the app is broken beyond repair; if it is DOWN the platform restarts it. Readiness says whether it may receive traffic now; if it is DOWN the platform stops sending requests until it recovers. Outside systems like the database may belong in readiness, never in liveness.
Lesson: Health checks with Spring Boot ActuatorI create a @Component that implements HealthIndicator and return Health.up(), down() or outOfService() with details from health(). The bean name without the suffix becomes the component name. In Boot 4 the interface is in org.springframework.boot.health.contributor.
@Value for a single value in one class. @ConfigurationProperties for any group of related settings: one typed, validated record with relaxed binding, lists, maps and durations, which I inject wherever needed. Typos in @Value keys fail silently when there is a default, which is another reason to prefer records.
I validate properties records with @Validated and constraints, avoid defaults for secrets, and add a prod-only check that calls getRequiredProperty for required keys like the database password. Start-up then fails with a clear report instead of failing on the first request.
Actuator adds operational endpoints. Over a public port I expose only health (with details hidden) and info. env, configprops, beans, loggers and heapdump can leak secrets, so they stay hidden or behind security on an internal port.
openapi (the version), info (title, version, contact), servers (base URLs), paths (each URL with its operations, parameters, request body and responses) and components (shared schemas and responses used through $ref).
It depends on who must agree. When several teams must sign off before coding, I write the contract first. When one team owns the code, I generate the spec from it, correct it with annotations and add a test so the docs cannot drift from the code.
Lesson: What OpenAPI and Swagger UI areThe docs are wrong, not the client: the ShopLite contract says creates return 201 with Location. I fix the spec for that operation and add a check so the documented status codes are tested against the contract.
Lesson: What OpenAPI and Swagger UI arespringdoc is a community project, not part of Spring Boot's dependency list. So I pin it in a Maven property and check its compatibility table whenever I upgrade Boot, since each springdoc line is built and tested for one Boot line.
Lesson: Adding springdoc-openapiWith springdoc.paths-to-match or paths-to-exclude patterns, or per feature with GroupedOpenApi beans. For one method I can also use @Hidden. ShopLite documents only /api/v1/**.
@Tag on the controller for the group, @Operation on each method for summary, description and operationId, @Parameter for parameter meaning and examples, @ApiResponse for each status code, and @Schema on DTO fields for descriptions and examples.
I declare each error once in the OpenAPI bean under components.responses, with its application/problem+json schema and an example, and refer to it with @ApiResponse(responseCode = "404", ref = "NotFound"). One change updates every endpoint.
No. @Operation, @ApiResponse and @Schema only feed the spec; they do not set status codes or validate anything. That is why the docs can lie, and why a test should compare them with the contract.
From Swagger UI with Try it out: it pre-fills examples, sends the real request and shows status, headers and body. For repeatable or shared checks I copy the curl command it shows, or import the spec into Postman as a collection.
Lesson: Testing your API from Swagger UII export /v3/api-docs to a JSON file and they import it as a collection: one request per operation with example bodies and a baseUrl variable. If they test another server, they change only baseUrl.
The method with -X, the full URL with every filled query parameter URL-encoded, an accept header, and for bodies a Content-Type header and -d with the JSON. It is written for bash, so on Windows I run it in Git Bash.
Code-first generates the spec from the code, as springdoc does; it is fast and fits one team that owns the API. Contract-first writes the spec first so several teams can agree before coding. With code-first I add annotations and a docs test; with contract-first I generate or test the code against the spec.
Lesson: Practice: OpenAPI documentationYes. springdoc copies @Max(50) into the schema even if @Valid is missing, so the rule is documented but not checked. Docs tests check the spec; I also need controller tests that send invalid data.
It should be. In ShopLite the annotations live next to the code and a test checks the generated spec, so a pull request that changes an endpoint without updating its docs fails the build, and reviewers see doc changes in the diff.
Lesson: Practice: OpenAPI documentationA pool keeps a few database connections open and lends them to requests. Opening a connection costs network round trips and a login, so reusing them makes each request much faster and limits how many connections MySQL has to serve. Boot uses HikariCP because it is small and fast.
Lesson: Connecting Spring Boot to MySQLNever in Git. application.properties has ${DB_PASSWORD}, and the value comes from an environment variable on my machine or from the server's secret store in production. If a password was ever committed, I change it, because Git history keeps it.
Hikari connects lazily, so a wrong URL or password would only show at the first real request, maybe hours after a deployment. One small query at startup makes the app fail fast with a clear error, which is what I want during a release.
Lesson: Connecting Spring Boot to MySQLFor a small schema where I want to see and control every SQL statement, plain SQL with JdbcClient is simple and fast, and nothing happens behind my back. JPA saves code for rich object graphs but brings its own traps. ShopLite moves to JPA in Section 16, and comparing the two is the point.
I pass a GeneratedKeyHolder to update(keyHolder) and read keyHolder.getKey().longValue(). I never use SELECT MAX(id), because another insert can happen in between and I would get the wrong id.
The repository returns the row count as a boolean, the service throws ResourceNotFoundException when it is false, and the global handler turns that into a 404 ProblemDetail. No layer does another layer's job.
Unchecked ones: RuntimeException, its subclasses, and Error. A checked exception commits by default. I either use rollbackFor = Exception.class or, as in ShopLite, make business exceptions extend RuntimeException.
It tells Spring and the driver that the transaction only reads. On MySQL the connection becomes read-only, so an accidental write fails, and the database can skip some work. In ShopLite each service class is readOnly = true and write methods add a plain @Transactional.
I check and change stock in one statement, UPDATE products SET stock = stock - ? WHERE product_id = ? AND stock >= ?, and look at the row count. MySQL locks the row during the UPDATE, so only one request can succeed; the other gets 0 rows and the service throws out-of-stock, which rolls back its transaction. Section 16 shows the alternative, SELECT ... FOR UPDATE with a pessimistic lock.
Two transactions each hold a lock the other needs, so both wait forever; MySQL then kills one. I update products in product-id order, so every order locks rows in the same order and the circular wait cannot happen.
Lesson: Transactions with @Transactional400 means the request itself is wrong, like a missing field. 404 means the resource in the URL does not exist. 409 means it clashes with current data, like a duplicate SKU or no stock. 422 means the fields are valid but refer to data that breaks a rule, like an unknown category.
Lesson: Practice: Spring Boot review and interview drillWhether the call passed the proxy (no this. call, public method, a Spring bean), whether the exception was checked or caught inside the method, and whether the code wrote inside a readOnly transaction. One of those explains almost every case.
Plain JUnit for rules, @WebMvcTest with MockMvc for controller status codes and error bodies, and only a few @SpringBootTest integration tests against a real test database for SQL and transactions. ShopLite runs the fast ones with mvn test and the database ones with mvn verify.
Each lesson has quizzes, practice and more interview questions on the same topic.
Open the first lesson