Loading...
Loading...
Backend interview guide
HTTP methods and status codes, headers, cookies, sessions, JSON and REST API design. Every answer is short enough to say aloud, and every question links to the lesson that teaches it.
60 questionsLast updated
An object is a set of named values in {}, where you look a value up by its key, like a customer's email. An array is an ordered list in [], where you go through the values by position, like the items of an order.
Either as a decimal number such as 17999.00 that both sides read into an exact decimal type like BigDecimal, or as a string "17999.00". The danger is a reader that stores it in a double: doubles cannot hold most decimal fractions exactly and lose digits on large amounts. I would document the choice in the API so every client reads it the same way.
I run it through a validator such as python -m json.tool or open it in an editor that checks JSON. The error gives a line and column; I look at that spot and just before it, because a missing comma or quote is noticed only when the next token starts. I fix one error at a time and validate again.
Jackson is the most widely used Java library for JSON: it converts Java objects to JSON and back. Spring Boot uses it to read JSON request bodies and write JSON responses, so most Java REST APIs use it, often without the developer calling it directly.
Lesson: Adding Jackson with MavenBy the imports: Jackson 3 uses tools.jackson.* and usually JsonMapper.builder(), Jackson 2 uses com.fasterxml.jackson.* and new ObjectMapper(). Jackson 2 code also has throws JsonProcessingException or try/catch around every call, because its exceptions are checked. Annotations look the same in both.
Usually not. The web starter already brings Jackson, and Spring Boot's dependency management picks a version that matches the Boot release, for example Jackson 3.1.5 with Boot 4.1.1. I would add a Jackson module explicitly only when I need one the starter does not include, and still leave the version to Boot.
Lesson: Adding Jackson with MavenYes, once it is configured it is thread-safe, and in Jackson 3 it cannot even be changed after build(). You should not create one per request: building a mapper is costly and its cache of class information would be thrown away each time. One shared instance, or the one Spring Boot provides, is the right choice.
{} for an object that clearly has data. What do you check?I check whether the class exposes its values to Jackson: private fields need public getters, or Jackson sees no properties. Jackson 3 does not fail on such "empty" classes by default, so the bug is silent. Lombok setups that forgot @Getter are a common cause.
When the JSON comes from a stream anyway, such as an HTTP response body or a large file. Jackson reads the stream in pieces, so I avoid first copying everything into one big String. I still close the stream with try-with-resources.
When the JSON's shape is unknown or changes, when I need only a few values from a large response, or when I want to inspect or modify JSON before mapping it. For data my code really works with, I prefer classes or records, because the compiler then checks field names and types for me.
Lesson: Lists, maps and nested objectsAn Order class with simple fields for id and status, a Customer field for the nested customer object and a List<OrderItem> for the items, each item with SKU, quantity and a BigDecimal unit price. Jackson then maps the nested JSON in one readValue call, and totals are computed with BigDecimal arithmetic, not doubles.
They declare the data in one line, are immutable, and come with equals, hashCode and toString. Jackson reads and writes them without getters, setters or annotations, and a DTO that cannot change after creation is easier to reason about, especially when it is shared between threads.
LocalDateTime is a date and a wall-clock time with no zone, so it does not identify one moment. Instant is an exact point on the UTC timeline. For an order or a payment I store and send an Instant, and convert to the user's zone, such as Asia/Kolkata, only for display.
Jackson 3 supports java.time out of the box and writes ISO strings by default. Jackson 2 needed the jackson-datatype-jsr310 module, and with it the default wrote dates as number arrays unless you disabled WRITE_DATES_AS_TIMESTAMPS. Spring Boot 3 already turned that off, so its JSON looked like ISO text too.
The safest way is to never put it in a response DTO: the entity has the hash, the response record does not. As an extra guard I put @JsonIgnore on any field that holds secrets. And I review responses in tests, because one forgotten field is a real leak.
"phone": null or leave the key out?Both are valid; what matters is that the API is consistent and documented. Leaving nulls out with NON_NULL makes responses smaller, but clients must treat a missing key as null. I usually set one rule on the mapper for the whole API rather than deciding per class.
02-11-2026. How do you handle it without changing your own API's date format?I use a separate DTO for the partner with @JsonFormat(pattern = "dd-MM-yyyy") on its date fields, or a separate mapper for that client. My own API keeps ISO-8601, and I convert between the partner DTO and my own types in one place.
It depends on whose JSON it is. For my own import files or internal requests, failing catches typos that would otherwise silently lose data. For responses from other services, I follow the tolerant reader rule and ignore extras, so their new fields do not break my client.
Lesson: Handling unknown or missing fieldsIn my code, right after reading, or with Bean Validation annotations in Spring Boot. Jackson only checks that the JSON is valid and that values fit their Java types. Business rules belong in a validation step that reports all problems together.
Lesson: Handling unknown or missing fieldsJackson's ACCEPT_FLOAT_AS_INT feature is on by default, so a decimal is truncated when read into an integer field. I disable it for data where a fraction is a mistake, so Jackson throws InvalidFormatException instead of silently changing the value.
I return response DTOs that simply do not have such fields, and I add @JsonIgnore on any secret that does sit in a serialized class. For input-only secrets like a sign-up password I use @JsonProperty(access = WRITE_ONLY), so they are read but never written to responses or logs.
Generic types are erased at run time, so Jackson only knows it needs a list and fills it with LinkedHashMaps. The cast happens later, where the code uses an element as a Product. A TypeReference<List<Product>> passes the full type and fixes it.
I check whether any money field is a double, in the DTO or in a Map where decimals become Double by default. Money must be BigDecimal, read exactly from the JSON number or string, with arithmetic done by add and multiply.
The HTTP method is already the verb. With nouns, one URL such as /products/3 supports GET, PUT, PATCH and DELETE, the API stays small and predictable, and tools like caches understand what each request does.
The path identifies a resource, so ids go there: /orders/14. Query parameters are optional modifiers of a list, such as filters, sorting and paging: /orders?status=PLACED&page=0.
I either make it an action sub-resource, POST /orders/16/cancel, or a new resource such as POST /orders/16/cancellations. For ShopLite I chose the action URL because cancelling has its own rule (only PLACED orders) and a side effect (restore stock).
POST to a collection creates a new member and the server picks its id; repeating it creates another one. PUT targets a known URL and sets its state; repeating it changes nothing more. In most APIs, POST creates and PUT updates.
Lesson: Mapping CRUD to methods and status codesThe state is the same, because DELETE is idempotent, but the response may differ. I return 404 so the client knows nothing is there; returning 204 again is also allowed. The important thing is to document the choice.
Lesson: Mapping CRUD to methods and status codes404. A 200 means "here is what you asked for"; returning null or {} makes every client add special checks and hides real bugs such as a wrong id.
/customers/{id}/orders over /orders?customerId={id}?When the customer is the natural way in, such as a "my orders" screen or a rule that customers see only their own orders. For a search where the customer is just one optional condition among others, I use a filter on /orders.
404, because the URL names something that is not there. If the parent exists but has no children, I return 200 with an empty list. Mixing these hides wrong ids.
Lesson: Nested resources: /customers/{id}/ordersAn order is a record of what was bought at what price. Changing lines after placing it would break stock counts and totals, so items are written once inside the order-placing transaction and only read afterwards.
Lesson: Nested resources: /customers/{id}/ordersWithout a limit, one request can load every row into memory and send megabytes over the network. A maximum page size keeps every response small and the server safe, however big the table grows.
Lesson: Filtering, sorting and pagination with query parametersSo clients can only sort by fields meant for it. Otherwise they could probe hidden columns through the order of results, and a server that builds SQL from the text risks SQL injection.
Lesson: Filtering, sorting and pagination with query parametersA normal 200 with an empty content and the real totals. It is not an error: the list exists, the client just asked beyond it, and the totals show that.
A standard JSON error format for HTTP APIs, RFC 9457, which replaced RFC 7807. It has type, title, status, detail and instance, uses the application/problem+json media type and allows extension members for extra data.
{"success": false}?The status line says success, so caches, monitoring, retries and clients that check status codes all treat a failure as a success. The error must be in the status code, with the details in the body.
Lesson: Consistent error response formatI catch that specific case and answer 409 with an already-exists problem and a clear sentence. It is an expected situation caused by the client's data, so it must not surface as a 500 with the raw database message.
A change that makes a correct existing client fail or behave wrongly: removing or renaming a field, changing its type or meaning, adding a required input or changing a status code. Adding fields or endpoints is not breaking if clients ignore unknown fields.
Lesson: API versioningI add the replacement first and send both, mark the old one deprecated in the docs and with Deprecation and Sunset headers, watch the logs, and remove it only in the next major version after the sunset date.
No. Only breaking changes get a new major version. Additions ship inside the current version, so clients are not forced to move for every new feature.
Lesson: API versioningPOST to the collection when the server chooses the id, which is the usual case: POST /products returns 201 with Location. PUT to a known URL fits when the client chooses the id, and because PUT is idempotent it is safe to retry.
I put the major version in the path, like /api/v1. Additions stay in the current version; only breaking changes get a new one, which runs side by side with the old version until a published sunset date.
The booking is POST /bookings. The server makes the seat check and the insert one atomic step, for example a unique constraint on trip and seat, so only one insert can win. The loser gets 409 with a problem body naming the seat.
First the status code, because it says whether the call worked. Then the body, to see that the data or the error message is right. Then the headers, such as Content-Type, and for a create, Location. I also glance at the time if the endpoint feels slow.
History is an automatic log of everything I sent, useful for finding yesterday's request. A saved request lives in a collection with a clear name, and I can organise, share and reuse it. Anything I will need again, I save.
Lesson: Postman: your first requestsNot by itself. For a list or search endpoint, 200 with an empty list correctly means "nothing matched". It is a bug only if data that should match exists, so I check the filters I sent and the data, for example an inactive product that the list hides on purpose.
Lesson: Postman: your first requestsContent-Type describes the body I am sending, such as application/json. Accept says which formats I want back in the response. A JSON POST usually sends both, but only a wrong Content-Type causes a 415.
PUT replaces the whole resource, so I send every field. PATCH changes only the fields I send, such as just the stock. If I send a partial body with PUT, a good API returns 400 listing the missing fields; a weak one may clear them.
Lesson: Sending JSON bodies and headersYes. Idempotent is about the state on the server, not the status code: after one DELETE or ten, the product is gone. The first call says 204 because it deleted something; later calls say 404 because nothing was there.
Lesson: Sending JSON bodies and headersThe environment value, because Postman picks the narrowest scope: local, then data, then environment, then collection, then global. I keep safe defaults in the collection and target-specific values in environments.
Lesson: Postman collections and environmentsIn the create request's post-response script I read the id from the response, pm.response.json().id or the Location header, and save it with pm.collectionVariables.set. Later requests use {{productId}} in their URLs, so the flow works for any id.
Then the API's example requests are versioned and reviewed with the code, new teammates can import them on day one, and a CI pipeline can run them. When an endpoint changes, the collection changes in the same pull request.
Lesson: Postman collections and environmentsThe important headers, such as Content-Type and Location after a create, the response time against a target, and the body: required fields, their types and values, list sizes, sorting. For errors I check the error format and message, not just the code.
Newman is Postman's command-line runner for exported collections. It lets the same tests run without the app, for example in CI after every push. It exits with code 1 when any test fails, which fails the build, and it can write JUnit reports.
Lesson: Saving tests in a collectionIt is fine as a warning, but the number must fit the target: a laptop and a shared test server differ. I keep the limit in an environment variable and keep the important checks, status and data, separate from the timing check so a slow day does not hide real results.
Lesson: Saving tests in a collectionI keep the body in a file and run curl --json @body.json URL, which sets POST, Content-Type and Accept. On older curl I use -d @body.json -H "Content-Type: application/json". Without the header, -d sends the form content type and a JSON API answers 415.
I keep them in environment variables and reference them, such as -H "Authorization: Bearer $TOKEN" or -u "user:$PASSWORD", so they are not in shell history or scripts. A fresh token comes from the login response with jq -r .accessToken.
--retry safe to use?For requests that can be repeated without side effects, such as GET, or idempotent PUT and DELETE. For a POST that creates something, a retry after a lost answer can create a duplicate, so I retry it only if the API supports an idempotency key or I check first.
Lesson: curl recipes for daily useEach lesson has quizzes, practice and more interview questions on the same topic.
Open the first lesson