Return to the article

Application brief

Context and intent

Build a backend-for-frontend (BFF) for a product-discovery client. The client needs a stable, client-oriented API, while product data is owned by the existing Order API and successful product queries must also be reported asynchronously for auditing.

The BFF is the system boundary that shields the client from those downstream integrations. It does not own product data. Instead, it:

  • exposes the client-facing HTTP interface described below;
  • translates and forwards product reads and writes to the Order API;
  • validates both client requests and downstream responses at the boundary;
  • converts downstream unavailability into the documented resiliency behavior; and
  • publishes a product-query event to Kafka after a successful availability search.

The purpose of this application is therefore not simply to proxy HTTP calls. It must preserve the BFF contract even when the Order API is slow, unavailable, or returns data that violates the requested business constraints. This behavior must be verifiable without requiring live Order API or Kafka infrastructure.

Solution architecture

Product BFF Orchestration

Figure 1: Product BFF orchestration

Responsibilities and boundaries

ComponentResponsibility
Product clientCalls the client-oriented product and monitor endpoints
Product BFFValidates, orchestrates, applies resiliency policy, records deferred requests, and publishes query events
Order APISystem of record for product reads and writes
Kafka product-queries topicCarries the first product returned by a successful availability query
Downstream consumersConsume product-query events; their implementation is outside this application’s scope

The monitor store and retry worker are internal BFF implementation concerns. The Order API and Kafka broker are external dependencies and must be replaceable with test doubles through configuration. Implementing the Order API, Kafka consumers, or a product database is out of scope.

Technology and delivery constraints

  • Implement a runnable Java application using Spring Boot and Maven.
  • Make the downstream Order API base URL configurable.
  • Make the Kafka bootstrap servers configurable.
  • Include a good README.md covering how to build, test, configure, and run the application.
  • Ensure the application starts successfully as a local HTTP service using the standard tooling for its technology stack.
  • Expose the health endpoint below so automated tooling can determine when the HTTP application is ready without invoking business operations.

Client-facing HTTP interface

By default, the application listens on port 8080 and implements exactly the four operations below. Enforce the stated parameter, header, body, media-type, and response constraints. Do not add operations or response statuses not listed here.

GET /health - application readiness

Return 200 application/json once the HTTP application is initialized and ready to accept requests. The response body must be exactly:

{
  “status”: “UP”
}

This is a shallow application-readiness check. It must not call the Order API or Kafka, inspect or mutate monitor state, publish messages, or perform other business side effects. Dependency readiness is managed separately by the runtime and test harness.

Shared data shapes

ProductBase is a JSON object with the following fields:

FieldTypeRequiredConstraints
namestringyesnone
typestringyesone of book, food, gadget, or other
inventoryintegeryesfrom 1 through 101, inclusive

Example:

{
  “name”: “iPhone”,
  “type”: “gadget”,
  “inventory”: 100
}

Product contains every ProductBase field plus the following fields:

FieldTypeRequiredConstraints
idintegeryesnone
createdOnstringyesYYYY-MM-DD date format

Example:

{
  “id”: 1,
  “name”: “iPhone”,
  “type”: “gadget”,
  “inventory”: 100,
  “createdOn”: “2025-01-01”
}

An error response is an application/json object with the following fields:

FieldTypeRequiredConstraints
timestampstringnonone
statusintegernonone
errorstringnonone
messagestringnonone

Example:

{
  “timestamp”: “2025-01-01T10:15:30Z”,
  “status”: 400,
  “error”: “Bad Request”,
  “message”: “Request validation failed”
}

POST /products - create a product

Accept a required Content-Type: application/json request body matching ProductBase.

Example request:

```http POST /products HTTP/1.1 Content-Type: application/json

{ “name”: “iPhone”, “type”: “gadget”, “inventory”: 100 } ```

Allowed responses:

StatusWhenResponse
201 CreatedThe Order API creates the productapplication/json body containing required integer field id, for example {”id”:1}
202 AcceptedThe downstream POST times outRequired Link response header and no response body
400 Bad RequestThe client request is invalidapplication/json error object

The Link header must match this exact structure, with a numeric monitor ID and an alphanumeric, space, underscore, or hyphen title:

Link: </monitor/123>;rel=related;title=monitor

GET /monitor/{id} - retrieve a deferred request

id is a required integer path parameter.

Allowed responses:

StatusWhenResponse
200 OKThe monitor can be returnedapplication/json monitor object described below
400 Bad RequestThe path parameter or request is invalidapplication/json error object
404 Not FoundA valid monitor ID is not presentapplication/json error object

A monitor object has the following fields:

FieldTypeRequiredConstraints
requestobjectnowhen present, must match the recorded-request shape below
responseobjectnowhen present, must match the recorded-response shape below

A recorded request has the following fields:

FieldTypeRequiredConstraints
methodstringyesnone
bodyobjectyescontains the original request body
headersarray of header objectsyeseach item uses the header-item shape below

A recorded response has the following fields:

FieldTypeRequiredConstraints
statusCodeintegeryesnone
bodyobjectyescontains the downstream response body
headersarray of header objectsyeseach item uses the header-item shape below

A header item has the following fields:

FieldTypeRequiredConstraints
namestringnonone
valuestringnonone

Completed-monitor example:

{
  “request”: {
    “method”: “POST”,
    “body”: {
      “name”: “iPhone”,
      “type”: “gadget”,
      “inventory”: 100
    },
    “headers”: []
  },
  “response”: {
    “statusCode”: 201,
    “body”: {
      “id”: 1
    },
    “headers”: []
  }
}

Before the retry completes, return the recorded request and omit the unavailable response. Once the retry succeeds, retain the response so subsequent monitor calls return both objects.

GET /findAvailableProducts - find available products

Request inputs:

NameLocationTypeRequiredConstraints
typequerystringnoone of book, food, gadget, or other
pageSizeheaderintegeryesnone
from-datequerystringyesYYYY-MM-DD date format
to-datequerystringyesYYYY-MM-DD date format

Example request:

GET /findAvailableProducts?type=gadget&from-date=2025-01-01&to-date=2025-11-28 HTTP/1.1
pageSize: 10

Allowed responses:

StatusWhenResponse
200 OKThe Order API returns a valid resultapplication/json array of Product objects; the array may be empty
429 Too Many RequestsThe downstream GET times outRequired integer Retry-After response header, set to 1, and no response body
400 Bad RequestThe client request is invalidapplication/json error object
502 Bad GatewayThe downstream response violates the requested filtersapplication/json error object
503 Service UnavailableThe Order API is unreachable, or Kafka publication fails or times outapplication/json error object

Example successful response:

[
  {
    “id”: 1,
    “name”: “iPhone”,
    “type”: “gadget”,
    “inventory”: 100,
    “createdOn”: “2025-01-01”
  }
]

Downstream Order API interface

Do not use an in-memory product store. Use these Order API operations over HTTP. Forward the product filters and page-size header received from the client when calling the downstream GET.

Downstream GET /products

The downstream endpoint accepts the same optional type query parameter, optional pageSize integer header, and optional from-date and to-date date query parameters described above. A successful response is 200 application/json with an array of Product objects. A bad request is 400 application/json with the error shape described above.

Example interaction:

GET /products?type=gadget&from-date=2025-01-01&to-date=2025-11-28 HTTP/1.1
pageSize: 10

```http HTTP/1.1 200 OK Content-Type: application/json

[ { “id”: 1, “name”: “iPhone”, “type”: “gadget”, “inventory”: 100, “createdOn”: “2025-01-01” } ] ```

Downstream POST /products

Send an application/jsonProductBase body. The downstream call requires:

  • a non-empty Authenticate API-key header; and
  • an Idempotency-Key header containing a UUID so retries do not create duplicates.

On success, it returns 201 application/json with an object containing required integer field id. It can also return 400 Bad Request, 415 Unsupported Media Type, or 422 Unprocessable Entity, each with an application/json error body.

Example interaction:

```http POST /products HTTP/1.1 Content-Type: application/json Authenticate: configured-api-key Idempotency-Key: 3d593d8d-6141-4c13-aad1-d0bc2f20ca42

{ “name”: “iPhone”, “type”: “gadget”, “inventory”: 100 } ```

```http HTTP/1.1 201 Created Content-Type: application/json

{ “id”: 1 } ```

Kafka message interface

For every non-empty successful GET /findAvailableProducts result, publish exactly one message with a body and no headers for the first returned product to the Kafka topic product-queries. Wait for the Kafka broker to acknowledge the publication before completing the 200 OK response; do not leave publication as unobserved background work.

Bound the wait for Kafka acknowledgement to one second. If publication fails or acknowledgement is not received within that period, return the documented 503 Service Unavailable error response instead of 200 OK.

The message content type is application/json. Its supported fields are:

FieldTypeRequiredConstraints
namestringnonone
inventoryintegeryesnone
idintegeryesnone
categoriesarray of category objectsnoeach item uses the category shape below

A category object has the following fields:

FieldTypeRequiredConstraints
idintegernonone
namestringnonone

Project the matching fields from the first downstream product as the Kafka message. For example, the successful response shown above produces:

{
  “id”: 1,
  “name”: “iPhone”,
  “inventory”: 100
}

Resiliency and business behavior

Deferred POST processing

  • When the downstream POST times out, return 202, record the request, and provide its monitor URL in the Link header.
  • Retry the same downstream request in a background thread pool every 1 second until it succeeds or 10 retries have been attempted.
  • Reuse the same idempotency key for every retry of one logical product-creation request.
  • When a retry succeeds, record the downstream response so the next monitor call returns the request and response.

GET throttling

  • When the downstream GET times out, return 429 with Retry-After: 1, and enforce the one-second retry interval.
  • When the downstream GET fails because the Order API is unreachable, including connection refusal, DNS failure, or connection reset, return the documented 503 Service Unavailable error response.
  • A later successful request follows the normal 200 response and Kafka publication flow.

Downstream response validation

  • When type was supplied, every returned product must have exactly that type.
  • When from-date was supplied, every returned product’s createdOn date must be on or after it.
  • When to-date was supplied, every returned product’s createdOn date must be on or before it.
  • Return the documented 502 error response if any returned product violates these conditions. Do not publish a Kafka message for an invalid downstream response.

Connectivity timeouts

  • The timeout for every POST or GET request to Order API should be configurable through an environment variable named ORDER_API_TIMEOUT_MS. If it is not provided, default to 500 milliseconds.

Tests

  • Automated tests must cover the specified contracts, resiliency behavior, downstream validation, and Kafka publication rules.

Return to the article