You are building a Bootstrap API that aggregates data from multiple services for a given user. Given a userId, the API must return a consolidated response containing the customer id, default payment card, and address.
Three pre-implemented services are available, each with Request and Response classes that extend HttpRequest / HttpResponse. Each getResponse() call may return a 500 status code on failure.
ServicesServicePurposeInputOutputUserServiceResolve customer infouserIdcustomerIdPaymentServiceFetch default cardcustomerIdlast_name, first_name, card_last_fourAddressServiceFetch mailing addresscustomerIdaddress string
userId → UserService → customerId → PaymentService └→ AddressService
UserService with the initial userId to obtain customerId.PaymentService and AddressService with the customerId (these can run concurrently — they have no dependency on each other).Core functionality
CustomerId, DefaultCard, and Address.Failure handling (discuss with interviewer)
customerId to key downstream calls. Return None (or equivalent) to signal the bootstrap cannot proceed.DefaultCard = None (default / missing card).Address = "" (empty string default).Design considerations
customerId. Serial calls double end-to-end latency for no gain.\{ "CustomerId": "cust_12345", "DefaultCard": \{ "last_name": "Smith", "first_name": "John", "card_last_four": "4242" \}, "Address": "123 Main St, San Francisco, CA 94102" \}
For the scored tests below, implement aggregate_bootstrap(user_resp, payment_resp, address_resp) which takes the three service responses as already-fetched dicts of the form \{"status": int, "body": ...\} and returns the consolidated response (or None if UserService failed). This keeps the logic under test deterministic; orchestration (concurrency, retries, timeouts) is a design discussion above.
Hint 1: Short-circuit on UserService If
user_resp.status != 200, there is nothing meaningful to return — nocustomerIdmeans Payment and Address cannot even be retried. ReturnNoneimmediately.
Hint 2: Treat downstream failures as defaults For each of PaymentService and AddressService, decide the default BEFORE reading the body. A clean pattern is: read
bodyonly whenstatus == 200, otherwise fall back to your default. This removes branching inside the response construction.
Hint 3: Concurrency in the real system In the real service you'd wrap PaymentService and AddressService calls in a thread pool / async gather so total latency is
max(payment_latency, address_latency)rather than the sum. Wrap each future with a per-call timeout and treat timeout/exception as a 500 response for the purposes of aggregation.
Full Solution ` def aggregate_bootstrap(user_resp, payment_resp, address_resp): # UserService is the hard dependency — without customerId we cannot proceed if user_resp.get("status") != 200 or not user_resp.get("body"): return None
customer_id = user_resp["body"]["customer_id"] # Payment: default card is None when the service failed default_card = None if payment_resp.get("status") == 200 and payment_resp.get("body"): default_card = payment_resp["body"] # Address: default is empty string when the service failed address = "" if address_resp.get("status") == 200 and address_resp.get("body"): address = address_resp["body"].get("address", "") return { "CustomerId": customer_id, "DefaultCard": default_card, "Address": address, }`
Why this shape
- The aggregator is pure: all I/O (calling real services, retries, timeouts) lives outside. That makes it trivially unit-testable and thread-safe on its own.
- UserService is a hard dependency because its output is the key for the other two services. A hard fail here returns
Noneso the caller can decide whether to surface an error, retry the whole bootstrap, or serve a cached response.- Payment and Address failures degrade gracefully because the product can still render something useful — a checkout page can prompt the user to re-add a card, and a profile page can show a blank address.
Real-world orchestration sketch
` import concurrent.futures
def bootstrap(user_id, user_svc, payment_svc, address_svc, timeout_s=2.0): user_resp = _call_with_timeout(user_svc.get, user_id, timeout_s) if user_resp.get("status") != 200: return None customer_id = user_resp["body"]["customer_id"]
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as pool: p_fut = pool.submit(_call_with_timeout, payment_svc.get, customer_id, timeout_s) a_fut = pool.submit(_call_with_timeout, address_svc.get, customer_id, timeout_s) payment_resp = p_fut.result() address_resp = a_fut.result() return aggregate_bootstrap(user_resp, payment_resp, address_resp)`
Where
_call_with_timeoutwraps the service call, catches exceptions, and returns\{"status": 500, "body": None\}on timeout or error. Bounded retries (e.g., 2 attempts with exponential backoff) can be added inside_call_with_timeoutfor transient 500s.Complexity
- Time:
O(1)for aggregation itself; end-to-end latency isuser_latency + max(payment_latency, address_latency)when calls run concurrently.- Space:
O(1)— the response is a constant-size dict.Thread safety
- The aggregator never mutates shared state, so it is thread-safe by construction.
- Concurrency lives in the orchestrator; futures are joined before aggregation, so no locks are needed in the response builder.