Sequential user stories, one at a time. Budget 45 minutes.
Reported for a Senior Software Engineer II interview in Berlin as "Implement 2 REST APIs".
Story: POST /orders creates an order for a customer at a restaurant, with items (name,
quantity, priceCents). GET /orders/{id} returns it.
Acceptance Criteria:
POST /ordersreturns201 Created, aLocationheader pointing at/orders/{id}, and the order:id,statusCREATED, the items andtotalCentstotalCentsis computed by the server (sum of quantity x priceCents); a total sent by the client is ignoredGET /orders/{id}returns the order, or404for an unknown id- Money is an integer number of cents, never a
double
Hints:
ServletUriComponentsBuilder.fromCurrentRequestUri().path("/{id}")builds theLocation- A
ConcurrentHashMap<UUID, Order>behind a small repository class is enough
Reported for an engineering manager round as "implement a CRUD API".
Story: A customer lists their orders, page by page. While an order is still CREATED they can
change its items or cancel it.
Acceptance Criteria:
GET /orders?customerId=...&page=0&size=20returns only that customer's orders, withpage,size,totalElementsandtotalPages; a page past the end is empty, not an errorPUT /orders/{id}replaces the items and recomputes the total- Cancel with either
DELETE /orders/{id}orPOST /orders/{id}/cancel: choose one and justify it - Status codes:
400for a missingcustomerIdor bad paging,404for an unknown order,409when updating an order that is no longerCREATED - Decide (and say) what cancelling an already cancelled order returns
Hints:
- A cancelled order still exists (the customer, support and the restaurant need it), which is an
argument for
POST .../canceloverDELETE - An update racing a cancel must not change a cancelled order:
ConcurrentHashMap.computeIfPresentapplies a change atomically
Predicted from Delivery Hero job ads and their Reliability Manifesto: clients get clear, consistent errors.
Story: Reject bad input with Bean Validation, and return every error as an RFC 9457
ProblemDetail (application/problem+json) from one @RestControllerAdvice.
Acceptance Criteria:
customerId,restaurantIdand itemnameare not blank;itemsis not emptyquantityis 1 to 50 (both ends allowed);priceCentsis positive; missing numbers are errors, not zeros- A
400body names each invalid field (for exampleitems[0].quantity) 404,409, malformed JSON and a non-UUID id are allProblemDetailbodies too- The same rules apply to
PUT
Hints:
@Validon the@RequestBody,List<@Valid OrderItemRequest>for nested items- Extending
ResponseEntityExceptionHandlerturns Spring's own 400s intoProblemDetailfor free - Once a handler has constraints on plain parameters (
@Minonpage), Spring validates the whole method and raisesHandlerMethodValidationExceptioninstead ofMethodArgumentNotValidException
Predicted: idempotency is one of Delivery Hero's reliability rules.
Story: The mobile app times out on POST /orders and retries. Support an Idempotency-Key
header so a retry never creates a second order.
Acceptance Criteria:
- Same key and same body returns the original order (same
id). Choose201or200and justify - Same key with a different body returns
422(or409: choose) - No key behaves as before; different keys create different orders
- Two simultaneous requests with the same key create exactly one order. Prove it with a test:
ExecutorServiceplus aCountDownLatchstart gate
Hints:
getthenputon a map is a race;ConcurrentHashMap.computeIfAbsentruns the creation once per key- Scope the key to the customer, so two customers who pick the same key do not collide
- Replaying the original response (
201, sameLocation) means the client needs no special case
Reported as a follow-up question: "What would you do if a customer repeatedly sends 500 requests to the same API in production?"
Story: Limit POST /orders per customer. Over the limit, return 429 Too Many Requests with a
Retry-After header.
Acceptance Criteria:
- A burst up to the bucket size succeeds, the next call is
429withRetry-Afterin whole seconds (rounded up) and aProblemDetailbody - After waiting, calls succeed again; one customer never uses up another's budget
- Rejected requests create no order; reads are not limited
- Tests move an injected
Clock, neverThread.sleep
Hints:
- An in-memory token bucket per customer, refilled lazily from
Clock.instant() - For the full exercise (fractional refill, backwards clocks, contention), see
java/rate-limiter
- How does rate limiting work across many instances? (Redis with a Lua script or
INCRplus TTL, or at the API gateway before the service) - Where are idempotency keys stored in production, and for how long? (Redis or a DB table with a unique constraint, TTL around 24 hours; what happens to a request still in flight?)
PUTvsPATCHfor changing the items: which did you use and why?- How would you secure this API? (JWT from the identity provider; authorization so a customer can
only see and change their own orders, which also means
customerIdcomes from the token, not the query string) - What is CORS, and when would this API need it? (reported question)
- What would you log and measure? (orders created, 4xx and 5xx by endpoint, 429s per customer, idempotent replays, latency percentiles; never log payment data)
- After an order is placed it must reach payment and the restaurant. How? (saga across services, transactional outbox so the event is published exactly when the order is committed)
- Ask for the status code before they write each endpoint.
200for a create, or500for an unknown id, is worth a follow-up question. - If the total is taken from the request, ask what a malicious client would send.
- A candidate who reaches for
synchronizedon the whole service for Task 4 has a correct answer; ask what it does to throughput across unrelated customers, then point atcomputeIfAbsent. - Should an idempotent replay spend a rate-limit token? There is no single right answer; the signal is whether they notice the question.
- If the candidate writes
Thread.sleepin the rate-limit test, ask how long the suite will take with fifty such tests. The injected clock is the expected answer.