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
Spring Boot 4 uses Jackson 3, which moved its core classes such as ObjectMapper to the tools.jackson packages. The annotations like @JsonProperty stayed in com.fasterxml.jackson.annotation. Old Jackson 2 imports compile only if Jackson 2 is on the classpath.
405 means the URL exists but not with that HTTP method; the Allow header lists what works, so I fix the method or the URL. 415 means the request body has a Content-Type the endpoint does not accept, usually because the client forgot Content-Type: application/json.
Yes. Idempotency is about the state on the server, not the response code. After the first or the tenth DELETE the resource is gone; the 404 on later calls only reports that.
Lesson: @GetMapping, @PostMapping, @PutMapping, @PatchMapping, @DeleteMapping@PathVariable reads a value from a {placeholder} in the URL path, like the id in /products/3; it identifies the resource. @RequestParam reads a name=value pair from the query string, like ?page=1; it filters, sorts or pages. Path variables are always present, query parameters are often optional.
Spring's type conversion fails, so it throws MethodArgumentTypeMismatchException and answers 400 before my method runs. The default body says only "Bad Request"; the log names the parameter and the value. A global exception handler can turn it into a clear error message.
First the parameter name: names are case-sensitive and unknown parameters are ignored, so categoryid instead of categoryId silently does nothing. Then whether the URL was quoted in the shell, because an unquoted & cuts off the rest of the query string.
Spring MVC picks an HTTP message converter for the request's Content-Type; for JSON that is Jackson. Jackson reads the body and creates the parameter object, a record in my code, before the controller method runs. If it cannot, Spring answers 400 with HttpMessageNotReadableException.
A record lists exactly the accepted fields, is immutable once Jackson builds it, and needs no getters or setters. It also keeps the API shape separate from my domain classes, so a client cannot set fields like id that only the server controls.
I read the server log line for that request: it names the exact problem, like an unexpected character, an unrecognized property or a wrong type. Common causes are a trailing comma, single quotes, a missing Content-Type, or shell quoting on Windows. Then I ask them to send the body from a file.
400 means the request itself is wrong, like broken JSON or a missing field. 404 means the resource in the URL does not exist. 409 means a valid request clashes with the current state, like a duplicate email or not enough stock. 422 means the fields are valid but refer to data that breaks a rule, like an unknown category id.
Lesson: ResponseEntity and status codesNo. ResponseEntity is an HTTP type, and the service should not know about HTTP, so it can be reused from a scheduled job or a message listener and tested without the web layer. The service returns data or Optional or throws a meaningful exception; the controller decides the status.
Entities carry internal fields (cost prices, audit columns, password hashes) that would leak, and their shape follows the database, so a column change would break clients. With lazy relations they can also trigger extra queries or serialization errors. I return a response DTO with exactly the contract's fields.
Lesson: Request and response DTOsThey carry different fields: the response has id and computed fields like categoryName, the request must not. Create, PUT and PATCH also have different rules for required fields. One shared class ends up with fields that mean different things in each direction.
OffsetDateTime, or Instant. A LocalDateTime has no offset, so a client cannot know which moment 21:05 means. OffsetDateTime writes 2026-09-21T21:05:00+05:30, which is one exact moment anywhere in the world.
Status 201 Created, a Location header with the absolute URL of the new resource, and usually the resource itself in the body. The client then knows it was created, where to find it, and its server-set fields such as the id.
The host and port change between a laptop, a test server and production, and behind a proxy the public URL differs again. A hard-coded URL is right only in one place; building it from the request, with forwarded headers behind a trusted proxy, is right everywhere.
Lesson: Returning 201 Created with a Location headerIt saves the client a second request: it sees the new id and every default the server applied, like isActive: true or a creation time. The Location is still useful for later requests and for clients that only store links.
@WebMvcTest loads only the web layer, so it needs no database and I control the service through mocks. @SpringBootTest loads the whole application, which checks that all parts work together but needs everything the app needs and is harder to point at one branch. I use many slice tests and a few full ones.
Both send requests through Spring MVC without a server. MockMvcTester, added in Spring Framework 6.2, works with AssertJ, so a test reads assertThat(mvc.get().uri(...)).hasStatusOk(), and it prints the whole exchange on failure. MockMvc uses perform(...).andExpect(...); I still read it in older code.
PUT sends the complete new resource and replaces the old one; PATCH sends only the fields to change. To change a product's price with PUT I send all six fields; with PATCH just {"price":1199.00}. PUT is idempotent, PATCH is not guaranteed to be.
I name the resource as a plural noun, give it a collection URL and an item URL with the id, and map list, read, create, replace, update and delete to GET, POST, PUT, PATCH and DELETE. Then I list every outcome with its status code, write request and response records, and check each row with a test.
Lesson: Practice: REST controllersThe URL exists but not with that method; the Allow header lists the methods that do work. Usually the client sent a create to an item URL or a delete to the collection URL, so I compare the request with the mappings.
Because then the rule exists only for that HTTP endpoint. A second endpoint, a scheduled job or a test has to copy it, and the copies drift. In the service it is written once, works for every caller and can be unit tested without starting a web server.
Lesson: What belongs in each layerIn the order service, before the new order is saved. The repository would give me the count (countByCustomerIdAndStatus), but the decision that 3 is the limit, and what happens when it is reached, is business logic. I would run the count and the save in one transaction so two orders at once cannot both pass.
Spring cannot build either one first, so startup fails with "The dependencies of some of the beans in the application context form a cycle" and a small diagram of the cycle. Circular references are refused by default since Spring Boot 2.6. I fix it by moving the shared logic into a third class below both, or by changing which side owns the check.
Lesson: What belongs in each layerBecause every getter becomes part of the API. Internal fields leak, renaming a field breaks clients, and names may not match the contract: a getter isActive() comes out as active. A response record makes the JSON an explicit decision that changes only when I change the record.
from() is fine when the response needs only the entity's own fields. When I map in several directions or need extra data, like a category name from another feature, I use a small stateless mapper class per feature. In ShopLite that is ProductMapper.
I collect the category ids of the page into a set, load all their names in one call, and then map each product with a map lookup. That is one query per page instead of one per product, which avoids the N+1 queries problem.
Lesson: Mapping between entities and DTOsNo. Each map operation is safe, but "check that the SKU is free, then save" is two steps, and another request can save between them. A real database closes that gap with a UNIQUE constraint, and the service turns the violation into a 409. In memory I could lock both steps together, but the plan is to rely on the database.
Lesson: An in-memory repository before the databaseSpring fails at startup: the service's constructor "required a single bean, but 2 were found". If I really need both, @Primary or @Qualifier choose one. When I am replacing an implementation, I delete the old class instead.
Package by layer groups classes by technical role: all controllers together, all services together. Package by feature groups them by business area: everything about products in product, everything about orders in order. The layers still exist inside each feature package.
Spring uses reflection, so it can create package-private classes and call package-private constructors. Access modifiers only limit other Java code. That is why my controllers and repositories have no public at all.
Things every feature shares and no feature owns: exception types, the page envelope, later the global error handler and configuration. A class that imports a feature must never go there, because then the dependencies point back up and features stop being independent.
Lesson: Package by layer vs package by featureI would still put it behind a service method. Reads have rules too, like hiding inactive products or other customers' data, and a direct repository call skips them. It also keeps the controller with one dependency, which makes it easy to test with a mocked service.
Lesson: Common layering mistakesThe service throws a business exception like NotFoundException or ConflictException. At the edge, Spring maps it to a status: with @ResponseStatus on the exception, or better with one @RestControllerAdvice that returns a consistent error body for the whole API.
In a throwaway demo maybe, but not in an API others depend on. Every getter becomes public JSON, field names follow the Java code instead of the contract, and binding request bodies to entities lets clients overwrite fields. A response record per resource is a few lines and removes all of that.
Lesson: Common layering mistakesBy feature: product, category, later customer and order, plus common for shared exceptions and the page envelope. Each feature exposes its service and DTOs and hides the rest, so the compiler stops other features from using its repository directly.
I refactor in small steps with the same requests as my check: move the stock rule and the saving into an order service, put storage behind a repository interface, add response records and a mapper, and leave the controller with one service call. The answers must stay identical after every step.
Lesson: Practice: layered architectureMalformed body, then field validation (400), then the resource in the URL (404), then rules about other data (422), then clashes like duplicates or state (409), then save. A fixed order means clients always get the same, most useful error for the same request.
Lesson: Practice: layered architectureBean Validation is the Jakarta standard: the jakarta.validation annotations and the Validator API. Hibernate Validator is the implementation that actually checks them; Spring Boot adds both with spring-boot-starter-validation.
I use BigDecimal, never double, with @NotNull, @DecimalMin(value = "0.00", inclusive = false) and @Digits(integer = 8, fraction = 2) to match a DECIMAL(10,2) column. That rejects zero, negatives and a third decimal before the value reaches the service.
I set message on every constraint, leave the field name out because the error response shows it, and use placeholders such as {min} and {max} so numbers are not repeated. Messages describe the rule ("must be 2-120 characters") and never leak internals like class names or regular expressions.
It is the exception for a failed @Valid @RequestBody (or @ModelAttribute) argument. It carries a BindingResult with one FieldError per broken rule: the field path, the rejected value and the message. A global handler can turn those into a clear errors list for the client.
Nothing visible: no error, no warning. The constraints are just annotations, so invalid data reaches the service, gets saved or causes a 500 later. I protect against it with a controller test per endpoint that sends a bad body and expects 400.
Lesson: @Valid on request bodiesSpring Boot hides details by default because they can leak class and method names. For development, spring.web.error.include-binding-errors=always shows them; for a real API I write a @RestControllerAdvice that returns a ProblemDetail with a short errors list.
HandlerMethodValidationException comes from Spring MVC's built-in method validation and becomes a 400. ConstraintViolationException comes from the older @Validated AOP proxy; Spring MVC does not know it, so without a handler it becomes a 500.
I bind the parameters into a record with @Valid and add either an @AssertTrue method or a class-level constraint. The class-level constraint is nicer for clients because it can report the error on a real parameter name such as minPrice.
I declare an annotation with @Constraint(validatedBy = MyValidator.class), @Target, @Retention(RUNTIME) and the three members message, groups and payload. Then I write a public class implementing ConstraintValidator<MyAnnotation, FieldType> with an isValid method that returns true for null.
Groups are marker interfaces that select which constraints run, for example OnCreate and OnUpdate, chosen with @Validated(OnUpdate.class). They work, but I usually prefer separate request DTOs per endpoint because each one shows its rules directly and nothing depends on remembering the right group.
Without Spring: either new SkuValidator().isValid(value, null) for a simple validator, or a Validator from Validation.buildDefaultValidatorFactory() to test the annotation on a record. I use parameterized tests for edge values and assert the message and the property path.
409 when the request clashes with the current state: a duplicate, not enough stock, an order that cannot change status. 422 when every field is valid but refers to data that breaks a rule, like an unknown category id or an inactive product.
Lesson: Validation in the service layer for business rulesIn the order service, because it needs stored data: it loads the address and compares its customer id, and throws a BusinessRuleException, which the contract maps to 422. The request DTO only checks that both ids are present and positive.
@Valid is the Jakarta annotation: it triggers validation of an argument and cascades into nested objects and list elements. @Validated is Spring's: on a parameter it does the same but can select validation groups, and on a class it turns on proxy-based method validation, which I avoid on controllers in Boot 3.2+ because it gives 500s.
I declare the list as List<@Valid Item> with @NotNull and @Size for the list itself. Then every item's constraints run, and errors carry the index, like items[2].quantity.
I check that the record has the constraints, that the controller parameter has @Valid, that there is no unread BindingResult, and for nested data that the element type has @Valid. Then I add a controller test that posts that body and expects 400, so it cannot come back.
It is Spring Boot's fallback HTML error page, shown when the client asks for HTML, as a browser does. API clients get JSON from the same /error endpoint. In a pure API I switch it off with spring.web.error.whitelabel.enabled=false.
500 says the server has a bug, so monitoring raises alarms and clients may retry a request that can never work. A missing product is the client's problem and should be 404 with a clear message. The log also fills with stack traces for something normal.
Lesson: Default Spring error responsesClients parse errors with code. With one shape they write one error reader for the whole API: read the status, show the message, highlight bad fields. With a different shape per endpoint every client needs special cases, and mistakes like a 200 for an error slip in.
Lesson: Default Spring error responsesUnchecked, extending RuntimeException. Checked exceptions force throws clauses through every layer, and Spring's @Transactional rolls back by default only for unchecked exceptions. I still catch checked ones from libraries, such as SQLException, and wrap them with the original as the cause.
@ResponseStatus goes on an exception class and fixes the status for every throw of that class. ResponseStatusException is one Spring exception that takes the status at the throw site. Both use the default error body and tie code to HTTP, so in a real API I prefer my own exceptions plus one global handler.
Each lesson has quizzes, practice and more interview questions on the same topic.
Open the first lesson