REST API versioning best practices, file-upload endpoints and where to store files, handling third-party rate limits and failures, cloud storage integration, a reactive API with WebFlux, consuming APIs with RestClient/WebClient, static plus dynamic pages, adding GraphQL, large uploads, and reactive consumption of external services.
Published September 25, 2026
These scenario questions test your integration judgement: which client, which storage, which API style, and how to stay up when a partner API misbehaves. Recommend the modern option first (RestClient, Spring for GraphQL, pre-signed URLs), and explain the trade-offs.
Short answer: Pick one strategy and apply it consistently. URI versioning (/api/v1/...) is the most common and operationally simplest. Header or media-type versioning (Accept: application/vnd.shop.v2+json) keeps URLs clean, but is harder to test and cache. Beyond the mechanism:
Deprecation and Sunset headers).Key points to cover:
@GetMapping(path = "/orders", version = "2")), with configurable resolution by path, header or media type.Learn it in depth → API Contract Design
Short answer:
@PostMapping(consumes = MULTIPART_FORM_DATA_VALUE) with a MultipartFile. Validate the size, type (by magic bytes, not just the extension) and name. Stream the content (getInputStream()), never getBytes() for large files. Generate your own storage key.Learn it in depth → S3
Short answer: Wrap each client in its own resilience policy:
RateLimiter, or a shared Redis bucket across instances), to stay under each provider's quota.Retry-After.resilience4j:
ratelimiter.instances.geoApi: { limit-for-period: 50, limit-refresh-period: 1s, timeout-duration: 200ms }
retry.instances.geoApi: { max-attempts: 3, wait-duration: 500ms, enable-exponential-backoff: true }
circuitbreaker.instances.geoApi: { failure-rate-threshold: 50, wait-duration-in-open-state: 30s }
Learn it in depth → Retry & Backoff Strategies
Short answer: Then decouple in time. Accept the request, persist it, and process it asynchronously from a queue, retrying until the API recovers, while telling the user it's "processing". Add idempotency keys to your calls, so retries never duplicate side effects (payments, bookings). Alert when the queue backs up, and put failed requests into a dead-letter queue for manual handling.
Short answer: Use the provider SDK (AWS SDK v2 S3Client, or Spring Cloud AWS's S3Template), wrapped in your own FileStorage interface, so the rest of the app doesn't depend on the vendor, and tests can use a fake or LocalStack.
public interface FileStorage {
StoredFile put(String key, InputStream content, long size, String contentType);
URL presignedDownload(String key, Duration ttl);
}
@Service
class S3FileStorage implements FileStorage {
private final S3Client s3; private final S3Presigner presigner; private final String bucket;
public StoredFile put(String key, InputStream in, long size, String type) {
s3.putObject(b -> b.bucket(bucket).key(key).contentType(type), RequestBody.fromInputStream(in, size));
return new StoredFile(key, size, type);
}
public URL presignedDownload(String key, Duration ttl) {
return presigner.presignGetObject(p -> p.signatureDuration(ttl)
.getObjectRequest(r -> r.bucket(bucket).key(key))).url();
}
}
Key points to cover:
application.yml.Short answer: Use spring-boot-starter-webflux (Netty), with controllers that return Mono<T>/Flux<T>, and make the whole chain non-blocking:
WebClient for outbound HTTP;Handle back-pressure, use timeouts and retries with Reactor operators, and never block the event loop. Offload unavoidable blocking calls with subscribeOn(Schedulers.boundedElastic()).
@RestController
@RequestMapping("/api/prices")
class PriceController {
private final PriceRepository prices; // ReactiveCrudRepository (R2DBC)
private final WebClient fx;
@GetMapping("/{sku}")
Mono<PriceView> price(@PathVariable String sku, @RequestParam String currency) {
return prices.findBySku(sku)
.zipWith(fx.get().uri("/rates/{c}", currency).retrieve().bodyToMono(Rate.class))
.map(t -> PriceView.of(t.getT1(), t.getT2()))
.timeout(Duration.ofSeconds(2));
}
}
Key points to cover:
RestTemplate or WebClient?Short answer: In new code, use neither by default:
RestClient (Spring 6.1+) is the modern synchronous client, with a fluent API.@HttpExchange) give declarative, Feign-like interfaces.WebClient is for reactive or non-blocking applications.RestTemplate still works, but it's in maintenance mode.@Bean
RestClient catalogClient(RestClient.Builder builder) { // Boot-configured builder (observability, converters)
return builder.baseUrl("https://catalog.internal")
.requestFactory(ClientHttpRequestFactories.get(ClientHttpRequestFactorySettings.DEFAULTS
.withConnectTimeout(Duration.ofSeconds(2)).withReadTimeout(Duration.ofSeconds(3))))
.build();
}
Product p = catalogClient.get().uri("/products/{id}", id)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, (req, res) -> { throw new ProductNotFoundException(id); })
.body(Product.class);
Key points to cover:
Short answer:
src/main/java/com/example/site/
SiteApplication.java (@SpringBootApplication)
web/TimeController.java (@Controller, GET /time)
src/main/resources/
static/index.html (served as-is at "/")
static/css/site.css
templates/time.html (Thymeleaf view, rendered per request)
application.yml
@Controller
class TimeController {
private final Clock clock;
TimeController(Clock clock) { this.clock = clock; }
@GetMapping("/time")
String time(Model model) {
model.addAttribute("now", ZonedDateTime.now(clock));
return "time"; // → templates/time.html
}
}
Key points to cover:
spring-boot-starter-web plus spring-boot-starter-thymeleaf. static/index.html is served automatically as the welcome page.Clock makes the time page testable.Short answer: Use Spring for GraphQL (spring-boot-starter-graphql), the official project built on GraphQL Java:
src/main/resources/graphql/schema.graphqls.@QueryMapping, @MutationMapping and @SchemaMapping, reusing your existing service layer.@BatchMapping (DataLoader).GraphQlTester, and explore with GraphiQL (spring.graphql.graphiql.enabled=true in development).type Query { order(id: ID!): Order }
type Order { id: ID!, status: String!, customer: Customer! }
type Customer { id: ID!, name: String! }
@Controller
class OrderGraphQlController {
@QueryMapping Order order(@Argument long id) { return orderService.find(id); }
@BatchMapping Map<Order, Customer> customer(List<Order> orders) { // one batched lookup, not N
return customerService.findForOrders(orders);
}
}
Common trap: "add the GraphQL Spring Boot starter" (the old third-party graphql-java-kickstart). Spring for GraphQL is the supported path today. REST and GraphQL can live side by side in the same app.
Short answer: Don't route large files through your app servers at all:
UPLOADING → PROCESSING → READY).If files must pass through the app: stream them (no buffering in memory), set multipart size limits and disk thresholds, use async processing, and put limits on concurrency.
Short answer: Use WebClient to get a Flux or Mono, then compose operators: map, filter, flatMap with bounded concurrency, buffer/window for batching, and timeout, retryWhen(Retry.backoff(...)) and onErrorResume for resilience. Back-pressure flows from the subscriber to the source.
Flux<Product> enriched = webClient.get().uri("/products?since={t}", since)
.retrieve()
.bodyToFlux(ProductDto.class) // streamed (NDJSON or JSON array)
.filter(ProductDto::active)
.flatMap(dto -> pricing.priceFor(dto.sku()).map(dto::withPrice), 16) // at most 16 concurrent lookups
.timeout(Duration.ofSeconds(10))
.retryWhen(Retry.backoff(3, Duration.ofMillis(200)).filter(this::isTransient))
.onErrorResume(e -> Flux.empty());
enriched.buffer(500).concatMap(repository::saveAll).subscribe(); // write in batches, sequentially
Key points to cover:
Flux..block() inside reactive flows. It defeats the model, and can deadlock the event loop.Q: What's the difference between Mono and Flux?
A: Mono emits 0 or 1 items, then completes or errors. Flux emits 0 to N items (possibly infinite). Both are lazy publishers, with back-pressure.
Q: When is GraphQL a better fit than REST? A: When many clients need different shapes of related data (mobile vs web), and you want to avoid over-fetching and chains of calls. It adds complexity in caching (no simple HTTP caching), rate limiting (by query cost) and N+1 prevention.
Q: How do you test code that calls an external API?
A: Unit-test the client with MockRestServiceServer (RestClient/RestTemplate), or WireMock. Add contract tests with the provider where possible. Test timeouts and error mapping, not just the happy path.
Q: What's a pre-signed URL? A: A time-limited URL, signed with your credentials, that lets a client upload or download one specific object directly from object storage, without giving them any cloud credentials.