A Spring Boot capability platform — registry, governance, versioning, and observability for your business capabilities.
Capstead is not another AI framework. It's the governance layer that sits around your AI (Spring AI, LangChain4j, or anything else). You keep writing normal Spring Boot methods; Capstead turns them into governed, discoverable, observable, versioned business capabilities — with per-capability cost tracking and budgets — from a single annotation.
Spring AI tells you about one model invocation. Capstead tells you about the business capability: how often it ran, which model it used, how many tokens it consumed, how much it cost, whether it succeeded, and which version executed.
📖 Read the research: Governing AI capabilities in Spring Boot →
Modern apps expose AI capabilities across many services and teams, and nobody can answer: what capabilities exist, who owns them, what version is live, and what do they cost? Frameworks like Spring AI and LangChain4j solve execution brilliantly. Capstead solves everything around execution:
| Spring AI / LangChain4j | Capstead | |
|---|---|---|
| Provider abstraction, structured output, tool calling | ✅ | reuses them |
| Capability registry + metadata + versioning | ❌ | ✅ |
/actuator/capabilities discovery |
❌ | ✅ |
| First-class execution records + cost/scorecards | ❌ (per model-call only) | ✅ |
| Per-model invocations + parent-child execution trees | ❌ | ✅ |
Durable execution history (JDBC) + /actuator/capabilityexecutions |
❌ | ✅ |
| Daily budgets & governance | ❌ | ✅ |
| Enforced provider hiding | ❌ | ✅ |
| Export capabilities as governed MCP tools | ❌ | ✅ |
<dependency>
<groupId>io.capstead</groupId>
<artifactId>capstead-starter</artifactId>
<version>0.3.3</version>
</dependency>That's it. Auto-configuration wires everything; no code changes beyond annotating a method.
Annotate any Spring bean method:
@Service
class LessonService {
@Capability(
name = "Generate Lesson",
domain = "EngineerPrep",
owner = "Content Team",
version = "2",
tags = {"lesson", "java"}
)
@DailyBudget("$25")
public Lesson generateLesson(String topic) {
// your normal logic — call Spring AI, LangChain4j, anything
return chatClient.prompt().user(topic).call().entity(Lesson.class);
}
}Now hit the actuator:
GET /actuator/capabilities # the catalog: name, domain, owner, version, tags
GET /actuator/capabilityscorecard # invocations, success rate, latency, tokens, cost
GET /actuator/capabilityexecutions # execution history: ids, parent, per-model invocations
GET /actuator/capabilitymetrics # Micrometer-backed stats (Prometheus-ready)
…and open the dashboard:
http://localhost:8080/capstead/
Expose the endpoints in application.yml:
management:
endpoints:
web:
exposure:
include: capabilities,capabilityscorecard,capabilitymetrics,capabilityexecutionsCapstead supports all three styles — mix and match in the same app.
1. Annotation — put @Capability on the method (as shown above). Best when you own the code and want the declaration next to it.
2. Config (YAML) — declare a capability without touching the bean, ideal for third-party or generically-named methods (generate / ask / review), or when you'd rather keep annotations out of your domain code:
capstead:
capabilities:
- name: "Generate Lesson"
bean: lessonService # the Spring bean name
method: generate # the method to govern
domain: Learning
owner: Content Team
version: "2"
tags: [lesson]
# parameter-types: [java.lang.String] # only to disambiguate overloaded methods3. Declarative — write no body at all. Annotate an interface method with @Capability + @Prompt, and Capstead generates the implementation (renders the prompt, routes the model, calls Spring AI, binds the result) — then governs it like any other capability:
@CapabilityClient
@ModelProfile("reasoning")
public interface LessonCapability {
@Capability(name = "Generate Lesson", domain = "Learning")
@Prompt("Generate a Java lesson for {{topic}}")
Lesson execute(String topic); // no body — Capstead writes it
}Model routing is config-driven via profiles, so capability code never names a model:
capstead:
ai:
profiles:
reasoning: { model: us.anthropic.claude-sonnet-4-6, temperature: 0.2 }Works with any model backend — no Spring AI required. Capstead calls a single CapabilityModelInvoker bean; you supply it (LangChain4j, a provider SDK, a plain HTTP client), or add capstead-spring-ai for a default Spring AI ChatClient implementation:
@Bean
CapabilityModelInvoker modelInvoker(MyLlmClient llm) {
return req -> llm.complete(req.model(), req.systemPrompt(), req.userPrompt());
}Full guide: docs/DECLARATIVE-CAPABILITIES.md.
If a method is declared more than one way, the annotation wins. There's also rule-based capstead.scan to promote many methods at once by package + name pattern. See a runnable example using every style in samples/.
Capstead does not measure tokens itself — it attributes Spring AI's existing token/model data to the business capability. Add the bridge:
<dependency>
<groupId>io.capstead</groupId>
<artifactId>capstead-spring-ai</artifactId>
<version>0.5.0</version>
</dependency>Configure per-model pricing (price per million tokens):
capstead:
cost:
models:
claude-sonnet:
input-per-million-tokens: 3.00
output-per-million-tokens: 15.00Now every capability's scorecard shows real token counts and estimated cost — with no code in your method. Once a capability's @DailyBudget is reached, further calls are blocked with a CapabilityBudgetException until the next day.
Not using Spring AI? The enrichment seam is open: call
CapabilityExecutionContext.recordModelInvocation(model, inputTokens, outputTokens, cost)once per model call, from wherever you make it — Capstead attributes each invocation to the capability currently executing on the thread.
Every @Capability call becomes a first-class CapabilityExecution with a unique id — the atom every scorecard, budget and dashboard is built on. In 0.3.3 the recorder became durable and structured:
- Per-model invocations. A capability may call the model several times (retries, multi-step, fan-out). Each call is captured as a
ModelInvocation(model, tokens, cost, timestamp), so cost is attributed per model — not just per capability. Token/cost totals are summed across invocations. - Execution trees. When one
@Capabilitycalls another, the nested execution is linked to its parent automatically (parentExecutionId) — an execution tree with no workflow engine. - History endpoint.
GET /actuator/capabilityexecutions # recent executions: id, parent, timing, outcome, invocations GET /actuator/capabilityexecutions/{id} # one execution + its nested capability calls - Recording modes.
capstead.executions.recording-mode: best-effort(default — recording never fails your business call)| sync | async. - Privacy by default. Inputs/outputs are not stored unless you opt in (
capstead.executions.capture-input/capture-output), and aCapabilityDataRedactorbean lets you strip secrets/PII. Attribute executions to a caller with aCapabilityPrincipalProvider.
The in-memory store is bounded and per-instance. Add capstead-jdbc to persist every execution (and its model invocations) to Capstead-owned tables:
<dependency>
<groupId>io.capstead</groupId>
<artifactId>capstead-jdbc</artifactId>
<version>0.3.3</version>
</dependency>capstead:
jdbc:
enabled: true
retention-days: 90 # 0 = keep foreverCapstead creates its own schema on startup (portable across PostgreSQL and H2), writes each execution and its invocations atomically, and trims old rows on a daily sweep — so scorecards and execution trees survive restarts and aggregate across every instance.
Capstead can publish your governed capabilities as Model Context Protocol (MCP) tools — so an MCP client (or an LLM) can discover and call them, while they stay versioned, owned, and budget-enforced. Plain MCP tools have none of that governance; Capstead carries it through.
<dependency>
<groupId>io.capstead</groupId>
<artifactId>capstead-mcp</artifactId>
<version>0.3.3</version>
</dependency>Each @Capability becomes an MCP tool: a stable name@version-derived tool id, a JSON input schema derived from the method signature, and a governance block (owner, domain, version, tags). Two actuator surfaces are added:
GET /actuator/capabilitymcp # tools/list — every capability as an MCP tool
GET /actuator/capabilitymcp/{name} # a single tool definition
POST /actuator/capabilitymcp/{name} # tools/call — body {"arguments": { ... }}
Invocations route through the capability's governing proxy, so @DailyBudget and execution recording apply exactly as for a direct call.
To expose the tools over a real MCP transport (STDIO, SSE, Streamable-HTTP), add the bridge plus any Spring AI MCP server starter:
<dependency>
<groupId>io.capstead</groupId>
<artifactId>capstead-mcp-server</artifactId>
<version>0.3.3</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server</artifactId>
</dependency>capstead-mcp-server registers a Spring AI ToolCallbackProvider built from your capabilities; the MCP server starter discovers it and serves every capability automatically — no per-tool code.
Capstead is dogfooded in production at engineerprep.io — an AI-powered
technical-interview-prep platform on Spring Boot. It governs about a dozen AI capabilities across two
domains — EngineerPrep (lessons, narration, tutoring) and Academy (scene & visualization
planning, subtopic & question generation, review, catalog and GitHub-repo generation) — and attributes
token cost per capability across Anthropic Claude and Amazon Nova (Bedrock), all visible on the
/capstead dashboard, /actuator/capabilityscorecard, and /actuator/capabilityexecutions.
Most of those are declared from config, without touching the bean's source — Capstead can promote an
existing method (even a generically-named generate / ask / review) to a governed capability:
capstead:
capabilities:
- { name: "Generate Narration", bean: lessonNarrationGenerator, method: generate, domain: EngineerPrep, owner: Content Team, tags: [lesson, narration] }
- { name: "Lesson Tutor", bean: lessonTutorService, method: ask, domain: EngineerPrep, owner: Content Team, tags: [tutor] }
- { name: "Review Question", bean: questionReviewer, method: review, domain: Academy, owner: Content Team, tags: [review] }No annotations, no code edits — each method becomes a governed, scored, budget-enforced capability.
(Prefer annotations? @Capability and config-driven declarations coexist; annotations win on conflict.)
Capstead wraps every capability with a Spring AOP advisor that turns each call into a first-class,
governed CapabilityExecution — then fans it out to recorders and exposes it over actuator, a
dashboard, and MCP. Capstead never executes the model itself; the model call (yours or the one Capstead
generates for a declarative capability) enriches the in-flight execution with model, tokens and cost.
flowchart TB
subgraph App["Your Spring Boot application"]
direction LR
Cap["@Capability method<br/>(you call the model)"]
Client["@CapabilityClient interface<br/>(Capstead calls the model)"]
end
subgraph Capstead["Capstead — auto-configured"]
Advisor["Capability AOP advisor<br/>(interceptor)"]
Ctx["Execution context<br/>(thread-local enrichment)"]
Registry["Capability registry<br/>(catalog)"]
Publisher["Execution publisher"]
end
Model["Model call<br/>Spring AI ChatClient · CapabilityModelInvoker · your own client"]
subgraph Recorders["Recorders"]
InMem["In-memory store"]
Micrometer["Micrometer metrics"]
Jdbc["JDBC — durable,<br/>cross-instance"]
end
subgraph Surfaces["Governance surfaces"]
Actuator["/actuator/capabilities<br/>/capabilityscorecard<br/>/capabilityexecutions"]
Dashboard["/capstead dashboard"]
Mcp["MCP tools"]
end
Cap -->|invoke| Advisor
Client -->|proxy| Advisor
Advisor -->|wraps / calls| Model
Model -.->|model · tokens · cost| Ctx
Advisor --> Ctx
Advisor -->|publish execution| Publisher
Publisher --> InMem & Micrometer & Jdbc
Registry --> Actuator
InMem --> Actuator
Jdbc --> Actuator
Actuator --> Dashboard
Registry --> Mcp
- Declare a capability three ways —
@Capabilityon a method, config (YAML), or a bodyless@CapabilityClientinterface. All flow through the same advisor. - Govern — the advisor enforces
@DailyBudget, times the call, and opens aCapabilityExecution. - Enrich — whoever calls the model (Spring AI's observation bridge, a
CapabilityModelInvoker, or your client) records model, tokens and cost onto the in-flight execution; nested calls form a tree. - Record — executions fan out to the in-memory store, Micrometer, and (optionally) durable JDBC.
- Expose — the registry and recorders back the actuator endpoints, the
/capsteaddashboard, and MCP tool export.
| Module | Purpose |
|---|---|
capstead-annotations |
@Capability, @DailyBudget — dependency-free |
capstead-core |
Public model: CapabilityMetadata, CapabilityExecution, CapabilityScorecard |
capstead-runtime |
Registry, discovery, execution capture, cost, budgets |
capstead-starter |
Spring Boot auto-configuration + actuator endpoints + dashboard + declarative capabilities (@CapabilityClient, provider-neutral) |
capstead-spring-ai |
Optional: token/model attribution from Spring AI observations + a default Spring AI ChatClient model invoker for declarative capabilities |
capstead-mcp |
Optional: export capabilities as MCP tools + /actuator/capabilitymcp |
capstead-mcp-server |
Optional: serve capabilities over a live MCP server via Spring AI ToolCallbackProvider |
capstead-jdbc |
Optional: durable, cross-instance execution + model-invocation persistence with retention |
- Provider-agnostic — Capstead never exposes
ChatClient, model names, or tokens in the capability API. A startup guard rejects any@Capabilitywhose signature leaks a provider type. - Business-first — you think in Generate Lesson, Approve Payment, not prompts and temperature.
- Convention over configuration — one annotation exposes a governed capability.
- Spring Boot native — auto-configuration, dependency injection, Actuator, Micrometer.
0.5.0. The open-source core is complete and tested: registry, metadata, versioning, discovery, first-class executions with per-model invocations and parent-child execution trees, cost estimation, daily budgets, actuator endpoints (catalog, scorecard, metrics, execution history), a dashboard, the Spring AI bridge, provider-neutral declarative capabilities (@CapabilityClient — works with any model backend via a CapabilityModelInvoker), MCP export (tool model, actuator, and Spring AI MCP server bridge), and an optional JDBC recorder for durable, cross-instance history with retention.
Apache License 2.0.