Stage 7 · Master
The API Gateway & Service Boundaries
Internal Calls & Context Propagation
Carry request identity, tenant scope, and deadlines downstream the same way every time or debugging turns into folklore.
Why Context Has to Survive the First Hop
A surprising number of backend systems do the hard work of generating request IDs, validating tenants, and setting timeouts at the edge, then lose most of that information the moment one service calls another. I wanted Fieldwork to avoid that from the start because the architecture already assumes a gateway in front of multiple services. The first internal hop is where good observability either stays intact or quietly dies.
For Fieldwork, the minimum metadata worth preserving is request ID, tenant ID, authenticated subject ID, and the caller's deadline. Those values explain who made the call, which tenant data the call is allowed to touch, and how long the rest of the chain may keep working before the request is already useless to the client. Recomputing or improvising those values per service would guarantee subtle inconsistencies later.
It is not just for tracing. Tenant scope, cancellation, retries, and idempotency decisions all get safer when every service sees the same upstream story about the request.
Putting Request Metadata into Context Once
The pattern I prefer is simple: the gateway middleware parses headers and validated JWT claims once, converts them into a typed request context value, and stores that value in context.Context. From there, every handler and downstream client reads the same structure. I explicitly rejected passing tenant IDs and request IDs around as loose function parameters everywhere. It looks explicit for the first few functions and becomes noise once every service method takes six infrastructural arguments before the actual business input.
Typed context values also keep us honest about what is globally relevant. If a value does not belong to the lifecycle of the entire request, it probably should not be in context. Tenant ID does. A pagination cursor for one handler usually does not. That distinction matters because context abuse is almost as bad as no context propagation at all.
Forwarding Context on Outbound Calls
Propagation fails most often at the client boundary. A service builds a fresh http.Request without the parent's context, or forwards a context but forgets the metadata headers that a downstream service expects. Fieldwork standardizes both: outbound HTTP requests use http.NewRequestWithContext, and a thin internal client writes the request ID, tenant ID, and subject ID headers from the same Metadata struct every time.
I did not want each service team to reinvent that boilerplate. When propagation is hand-written at every call site, some requests miss a tenant header, some mutate header names, and some never forward deadlines. Then a downstream timeout looks random when it was really a missing context chain. A shared internal client makes the right path boring.
Deadline and Cancellation Are Part of the Contract
The most overlooked part of propagation is not the headers; it is the deadline. If the browser has already given up on a request, downstream services should stop working too. Otherwise you get zombie work: a project summary request times out at the gateway, but tasks and memberships keep chewing through database queries for a result nobody will read. In Go, context cancellation makes this cheap to propagate if you do not throw it away.
Fieldwork treats upstream deadlines as budget, not suggestion. A service may carve out a slightly shorter child timeout for a database call or downstream HTTP request, but it should not casually extend the deadline beyond what the caller gave it. I rejected that pattern because it breaks the mental model of end-to-end latency. If every hop extends timeouts independently, the system stops behaving like one request and starts behaving like a pile of uncoordinated retries.
| Propagation behavior | Short-term convenience | Long-term effect |
|---|---|---|
| Fresh background contexts per call | Easy to code | Loses cancellation, tracing, and tenant scope |
| Manual header passing only | Works for some debugging | Deadlines and cancellation still disappear |
| Shared context plus standardized headers | Slightly more upfront plumbing | Consistent request lineage across services |
context.Context is a request-scoped value, not application state. Pass it into each operation. Reusing an old context later is how cancellations, deadlines, and tenant metadata leak across requests.
What Fieldwork Standardized
Fieldwork standardizes request metadata at the gateway, stores it in context.Context as a typed struct, and forwards it through a shared internal HTTP client that also preserves cancellation and deadlines. Request ID, tenant ID, and subject ID travel on every downstream call, and services fail early when that contract is broken.
The rejected alternative was informal propagation: some headers here, some helper parameters there, and a few new background contexts when things got awkward. That works until the first production debugging session that spans three services and one timeout. We picked the boring standard because cross-service debugging is already hard enough without every hop telling a different story about the same request.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.