Following up on the Dynamic CDI Test Bean Addon, this post introduces another CDI extension built the same way: cdi-flow watches a CDI application do its work and writes the sequence diagram afterwards, from what actually happened. No annotations on your beans, no drawing tool — one dependency.
It also closes a loop. The Test Bean post ended with a note that the addon had served as a validation run for jawelte, and that there would be more on that soon. This is part of the "more".
Why Another Addon?
You know the picture in the wiki. Six boxes, some arrows, drawn the week the service was designed and quietly wrong ever since — a call that moved, a cache that appeared, an event nobody put on the slide. Nothing tells you it's rotted, because a drawing has no relationship to the code that outgrew it.
cdi-flow inverts that. While the container boots it applies a recording interceptor to your beans, records every public method call of the resulting chain, and as soon as the outermost call returns writes it out as a Mermaid or PlantUML sequence diagram. What you get isn't a model of your application. It's a transcript.
loop 3 times is three tags going through the normalizer, and [event] is a CDI event reaching its observer.cdi-flow.enabled=false.
What You Get
- A diagram per outermost call — Mermaid or PlantUML, written as the call returns
- The awkward bits handled — repeated calls folded into a
loop N timesblock, CDI events drawn with their own arrow, exceptions marked on every frame they travelled through - Clean names — proxies and interceptor subclasses never reach the diagram, so you see the classes you wrote
- Hotspots — a marker on the call that is actually slow, not on everything containing it
- Whole use cases — a series of HTTP requests collected into one diagram and one Markdown document
Quick Start
The artifacts are not on Maven Central — they live in a plain Maven repository served over GitHub Pages, so you declare it once:
<repositories>
<repository>
<id>os890</id>
<url>https://os890.github.io/os890-maven-repo/</url>
</repository>
</repositories>
Then the dependency. For Weld, OpenWebBeans or any full CDI container it's the jar and nothing else:
<dependency>
<groupId>org.os890.cdi.uml</groupId>
<artifactId>dynamic-cdi-flow-renderer</artifactId>
<version>0.9.0</version>
</dependency>
Quarkus resolves its beans while it builds the application and never runs a portable extension, so it gets cdi-flow-quarkus instead — same coordinates, same version. It records in dev mode and in tests with no configuration; a production build records nothing unless you write cdi-flow.enabled=true down explicitly, which is the point.
Two properties are worth setting. Diagrams default to the temp directory, and on a full Jakarta EE server you'll want to say which beans are yours — the container there also holds the REST layer and the CDI implementation:
cdi-flow.output-directory=target/flow-diagrams
cdi-flow.include-pattern=com\\.acme\\.order\\..*
Then trigger one entry point and read the file. That's the whole setup.
Recording Use Cases, Not Just Calls
A flow ends when its outermost call returns, and an HTTP request is an outermost call on its own thread. So one browser-driven use case does not produce one diagram — it produces a series of them. Labelling ties that series together, and on the test side the whole integration is one line:
await context.setExtraHTTPHeaders({ 'X-Flow-Label': testInfo.title });
Every flow recorded while that request is handled is filed under that use case; in process, FlowLabel.set("an order is placed") does the same.
What lands on disk is a small report rather than a pile of files: a use-cases.md with every use case and its diagram inline, plus a directory each holding the combined diagram and the chains it's made of. Identical chains are collapsed and counted — a use case that checks the session before every request reads the same list four times.
Two Runtimes, One Recording
The repository ships the same CRUD application twice — same beans, same front end, same four Playwright specs — once on Quarkus and once deployed into TomEE, one ./run.sh each. Driven through both, with timings and thread names normalized away, the combined diagrams come out identical for all four use cases, line for line. One thing genuinely differs: on Quarkus the extension knows which beans are yours, and on a server it cannot — which is why the Jakarta example needs that include-pattern. What a build-time extension works out for you, a portable extension has to be told.
A Recording Is Data, Not Only a Picture
Everything above ends in a file you look at. But the diagram is rendered from a recorded model, and that model is available first. Implement one method and every finished call chain is handed to you:
public interface FlowSink {
void onFlowRecorded(CallFlow flow);
}
Register it with FlowSinks.register(sink), or just make it a CDI bean. CallFlow gives you the whole tree, the durations and the hotspot flags, and CombinedFlowDiagram.of(flows, format, title) renders several collected flows as one diagram.
Which raises the obvious question: if a recording is data, and the same call chain renders to the same diagram every time, why only ever read it?
Turning a Recording Into an Assertion
That's what jawelte's flow-assert-module does — the "more on jawelte" I promised. jawelte is a separate project, a test framework for CDI applications, and this module consumes cdi-flow rather than being part of it. It records the flow of a test method and compares it against a diagram checked in beside the test:
@EnableFlowAssert
class OrderServiceFlowTest {
@Inject
private OrderService orderService;
@Test
@ExpectedFlow //uses: flows/OrderServiceFlowTest/placesOrder.mmd
void placesOrder() {
assertThat(orderService.placeOrder("SKU-1", 2)).isEqualTo("SKU-1@5");
}
}
An ordinary test pins what a call returned. This pins how it got there. Reorder two calls or route one through a different collaborator and the failure names the step that moved. Durations, timestamps and thread names are rendered but never compared — compare them and the test is flaky by construction.
For cdi-flow it's the useful kind of proof: the module consumes the recorder through FlowSink and CombinedFlowDiagram and nothing else — no fork, no reflection into internals.
What It Does Not Do
- Self-invocation is not recorded —
this.other()never leaves the instance, so no interceptor sees it. Inherent to interceptor-based recording, not a bug - A flow does not cross threads — each thread records its own, so an asynchronous observer gets a diagram of its own
- Only public methods are recorded, and beans from producer methods are not managed beans, so the container never intercepts them
- Argument values are never recorded — only parameter types, so no application data ends up in the files
The addon is compiled against the CDI API and SPI only. Published at 0.9.0 with sources and javadoc: dynamic-cdi-flow-renderer, cdi-flow-quarkus, cdi-flow-jaxrs for the use-case header, and cdi-flow-lite for CDI-Lite containers.
AI-Assisted Development
Like the Dynamic CDI Test Bean Addon and the MicroProfile Dashboard Addon, this was developed with an AI co-pilot — every authored commit carries the co-author trailer, the exceptions being the initial commit and the two the release plugin writes itself. Five days from the first commit to the 0.9.0 tag, across roughly 170 Java files and 229 tests that run against Weld and OpenWebBeans in turn.
Two things made the workflow work better than usual. The output is checkable — a recorded diagram either parses and balances its activation blocks or it doesn't, so an agent iterates against something firmer than my reading of the code. And the two demo applications turned "does this behave portably" from a claim into a test.
It also ships a SKILL.md, same idea as the Test Bean addon: drop it into ~/.claude/skills/ and the assistant knows which module a container needs and how to narrow what is recorded. The security hooks were in force throughout.
Links
- Source code: github.com/os890/dynamic-cdi-flow-renderer
- Artifacts: os890.github.io/os890-maven-repo —
0.9.0, with sources and javadoc - The assertion module: jawelte, under
modules/flow-assert-module - License: Apache License 2.0
Previous posts in this series: MicroProfile Dashboard Addon | OpenAPI GUI Addon | Upgrading 30+ Projects to Java 25 | Hardening Claude Code | Dynamic CDI Test Bean Addon



No comments:
Post a Comment