Intend: Architecture of an Intent-Driven API Client
"Every time you send a request, you're typing the same headers. What if the client just knew?"
The spark
I was studying how HTTP headers get resolved — Content-Type, Authorization, the Sec-Fetch-* family — and realized: for most API calls, headers are entirely derivable from the method, payload shape, and auth strategy.
Then I looked at how existing clients work. Postman, Insomnia, HTTPie — all script-based. They hand you a text box and say "fill it in." None of them actually reason about what headers your request needs.
So I thought: what if headers were resolved dynamically? What if you expressed your intent — POST this JSON with Bearer auth — and the system derived the protocol for you?
That's Intend.
Core abstraction
Everything starts with one record:
public record RequestIntent(
Method method,
URI url,
Object payload,
AuthStrategy auth,
boolean forceNew,
String env
) {}
Six fields. No headers. Just intent.
The header engine
The heart of Intend. Takes a ResolutionContext and runs it through an ordered pipeline of HeaderProvider plugins.
The SPI contract
public interface HeaderProvider {
int getOrder();
boolean supports(ResolutionContext context);
HeaderResolution resolve(ResolutionContext context);
}
Three methods. getOrder() sets priority. supports() gates activation. resolve() produces headers.
The pipeline
public Map<String, String> execute(ResolutionContext context) {
Map<String, String> headers = new LinkedHashMap<>();
for (HeaderProvider provider : providers) {
if (provider.supports(context)) {
HeaderResolution r = provider.resolve(context);
if (r.success()) headers.putAll(r.headers());
}
}
return headers;
}
Five built-in providers:
| Order | Provider | Activates when | Produces |
|---|---|---|---|
| 10 | ProtocolProvider | Always | Content-Type, Accept, User-Agent |
| 50 | IdempotencyProvider | POST / PUT / PATCH | Idempotency-Key, X-Request-ID |
| 90 | ApiKeyProvider | Auth = API_KEY | X-API-KEY |
| 90 | BasicAuthProvider | Auth = BASIC_AUTH | Authorization: Basic |
| 91 | BearerTokenProvider | Auth = BEARER_TOKEN | Authorization: Bearer |
Want to add a new header? Implement HeaderProvider, give it an order, done. Zero changes to core.
Content-Type auto-detection
The ProtocolProvider inspects the payload:
if (payload.toString().trim().startsWith("{")
|| payload.toString().trim().startsWith("[")) {
headers.put("Content-Type", "application/json");
} else if (payload.toString().trim().startsWith("<")) {
headers.put("Content-Type", "application/xml");
} else {
headers.put("Content-Type", "text/plain");
}
Starts with { → JSON. Starts with < → XML. Otherwise plain text. Deterministic, correct for 99% of use cases.
Data flow
Template engine
URLs and bodies support {{placeholder}} expressions resolved before execution:
| Placeholder | Output |
|---|---|
{{uuid}} | 550e8400-e29b-41d4-... |
{{timestamp}} | 2026-03-02T14:30:00Z |
{{randomInt}} | 427 |
{{randomEmail}} | user_3847@example.com |
Response chaining
Define a capture rule: USER_ID=/id
Response: {"id": "abc-123"} → stores USER_ID = abc-123
Next request: https://api.example.com/users/{{USER_ID}} → resolves to .../users/abc-123
No copy-pasting. No environment variables.
Authentication
Declarative. Pick a strategy from a dropdown. Credentials come from environment config:
{
"devUrl": "http://localhost:8080",
"devKey": "dev-api-key-here",
"prodUrl": "https://api.example.com",
"prodKey": "prod-api-key-here"
}
Switch environments → credentials switch automatically.
Execution layer
Built on java.net.http.HttpClient:
| Setting | Value |
|---|---|
| HTTP version | HTTP/2 with fallback |
| Connect timeout | 10s |
| Request timeout | 30s |
| Streaming | > 10 MB triggers disk download |
Result:
public record ExecutionResult(
int statusCode,
String body,
long timeMs,
long sizeBytes,
String statusCategory,
Map<String, String> requestHeaders
) {}
Persistence
Everything local. No cloud. No accounts.
~/.intend/
├── history.json # Request history
├── intend-config.json # Environment config
└── intend-state.properties # Idempotency keys
Delete ~/.intend/ → clean slate.
DI wiring
Providers are simple value objects — manually instantiated in a @Configuration class:
@Bean
public HeaderEngine headerEngine(StateRepository stateRepo) {
return new HeaderEngine(List.of(
new ProtocolProvider(),
new ApiKeyProvider(),
new BasicAuthProvider(),
new BearerTokenProvider(),
new IdempotencyProvider(stateRepo)
));
}
Order in this list doesn't matter — HeaderEngine re-sorts by getOrder().
The takeaway
Every API client treats headers as a user responsibility. Intend treats them as a system responsibility. Declare intent. Get protocol.
Built with intent.