Static analysis tool for Java/Spring codebases, built on top of Spoon and JGraphT.
Primary use case: supporting modernisation of legacy Spring applications by mapping dependencies and detecting patterns that make splitting into independent services difficult.
Every report is deterministic — the same source tree always yields the same graph, the same risk paths, the same metrics — and the JSON, CSV and GEXF outputs are structured for machine consumption, so they double as grounding data for an AI assistant planning the modernisation work (how to use them).
See FEATURES.md for a detailed explanation of every analysis — the algorithms used, why each matters for modernisation, and how to apply it.
The analyser identifies @Transactional methods (Spring, javax, jakarta) that — directly or through a chain of calls — reach an external system while the database transaction is still open.
Why this matters. Holding a database connection open during an HTTP call or a message publish ties two latency domains together: a slow downstream service delays the transaction, keeping the connection checked out and potentially exhausting the pool. It also makes the two systems harder to separate into independent services later.
How it works. Starting from each @Transactional root, the analyser performs a BFS over the call graph, following every reachable method. When it finds a node that invokes a known external client, it records the risk together with the full call path from root to call site.
The traversal respects transaction propagation boundaries: if a called method is annotated with REQUIRES_NEW, NOT_SUPPORTED, or NEVER, the branch is not followed (the outer transaction is suspended there, so the risk does not apply).
Detected external calls:
- HTTP:
RestTemplate,WebClient,RestClient,FeignClient,java.net.http.HttpClient, OkHttp, Apache HttpClient - Messaging:
KafkaTemplate,RabbitTemplate,JmsTemplate
JDBC and JPA calls inside a transaction are expected and are not flagged.
Interface resolution (CHA). Spring codebases typically separate the @Transactional BO layer from the data-access layer through interfaces. A plain call-graph scan stops at the interface type and never reaches the concrete implementation where the real I/O happens. After scanning all types, the analyser performs a Class Hierarchy Analysis pass: for every call to an interface method, it adds edges to every concrete implementation found in the same source tree. This ensures that risks hidden behind interface boundaries — such as a NotificationService interface whose implementation calls a REST endpoint — are not silently missed.
An SCC is a group of methods where every method can reach every other method through the call graph — i.e. there is a call cycle among them. The analyser finds all SCCs with more than one member using the Kosaraju algorithm.
Why this matters. Methods in the same SCC are mutually dependent: you cannot extract any one of them into a separate service without breaking a dependency on at least one of the others. Large SCCs are the primary obstacle to splitting a monolith and need to be untangled first.
For each package, the analyser counts:
- Ca (afferent coupling) — number of other packages that depend on this package (incoming edges across the package boundary).
- Ce (efferent coupling) — number of packages this package depends on (outgoing edges across the boundary).
From these it computes:
I = Ce / (Ca + Ce) range [0, 1]
The concept of measuring afferent/efferent coupling originates in the structured design work of Constantine & Yourdon (1974); the instability formula applied to OO packages was formalized by Robert C. Martin in the late 1990s.
| Value | Meaning | Implication |
|---|---|---|
| I ≈ 0 | Maximally stable — many depend on it, it depends on nothing | Core API; hard to change without breaking callers |
| I ≈ 1 | Maximally unstable — nothing depends on it, it depends on many | Good extraction candidate; changes are self-contained |
Packages with high instability and few afferent dependencies are typically the safest starting point for service extraction. Packages with low instability are the shared kernel — stabilise them as internal APIs or extract them last.
On top of the raw numbers the analyser classifies each application package into one of three actionable modernisation archetypes, reported under packageCoupling.hotspots:
| Archetype | Shape | Principle | Action |
|---|---|---|---|
unstableHubs |
high Ca + high I |
Stable Dependencies Principle violation | Bottleneck — decompose first |
extractionCandidates |
low Ca + high I |
Low blast radius | Peel off as a service first |
stableCores |
high Ca + low I |
Shared kernel | Harden behind ports, extract last |
Each entry has a score ranking it within its archetype. See FEATURES.md §5b for thresholds and the full scoring formulas.
The analyser identifies two structural patterns that create conditions for database lock contention or deadlock.
Nested REQUIRES_NEW. When a @Transactional method calls — directly or through a chain — a method annotated REQUIRES_NEW, the outer transaction is suspended but keeps its DB locks while the inner transaction opens a second connection. If both transactions touch the same rows, or if the pool cannot supply two concurrent connections, the result is deadlock or connection starvation.
Cyclic @Transactional clusters. When two or more @Transactional methods belong to the same call cycle (SCC), concurrent requests can acquire row locks in different orders, satisfying the classical conditions for deadlock. Resolving the cycle also removes the locking hazard.
Static analysis cannot determine which rows are locked at runtime. These findings are structural risk patterns to review, not confirmed bugs.
# Single project
java -jar build/libs/carve-dev-SNAPSHOT.jar <source-root> [options]
# Multi-project (analyse two or more source trees together)
java -jar build/libs/carve-dev-SNAPSHOT.jar \
--source <name>:<path> --source <name>:<path> ... [options]
Options:
--source <name>:<path> Named source root (repeatable, for multi-project analysis)
--classpath <path> Colon-separated JARs for type resolution
--java <version> Java compliance level for source parsing (default: 21; the
ceiling is whatever the bundled Spoon/ECJ supports)
--encoding <name> Source file encoding (default: UTF-8; use ISO-8859-1 for legacy codebases)
--markers <file> Properties file with custom external-call markers (see below)
--print-risks Print transaction risk call stacks to console
--print-paths Print the 10 longest call chains to console
--print-cycles Print cyclic method clusters to console
--print-lock-risks Print DB lock/deadlock risk patterns to console
--dot Also write method-level call-graph.dot (Graphviz)
--output <dir> Output directory (default: current directory)Arguments are validated up front: a missing source root, a non-existent --markers
or --classpath entry, an --output that is not a directory, or an invalid --java
/ --encoding value fails immediately with a clear message and a non-zero exit code,
rather than a stack trace partway through the run.
--source and --classpath serve different levels of the dependency graph:
| Code | How to include | What Spoon sees |
|---|---|---|
| Your own modules | --source name:path |
Full AST — method bodies, annotations, generics |
| Third-party JARs | --classpath path/to.jar |
Type signatures only — no bodies |
| Everything else | (neither) | Nothing — unresolved references, degraded accuracy |
Rule of thumb: every module whose internal logic matters to the analysis should be a --source root. Everything else that is needed only for type resolution (Spring JARs, vendor SDKs, utility libraries) goes on --classpath. Never put the same code on both — if a module is already a --source root its compiled JAR on --classpath would duplicate the types.
java -jar carve.jar /path/to/myapp/src/main/java \
--classpath /path/to/myapp/lib/spring-context-6.1.jar:/path/to/myapp/lib/hibernate-core-6.jar \
--java 17 \
--encoding ISO-8859-1 \
--markers my-markers.properties \
--print-risks \
--output ./reportsHere --classpath lists the framework JARs the application depends on. Spoon cannot see their source but uses them to resolve method signatures and supertype hierarchies, which improves CHA accuracy.
A common pattern in Spring applications is a web module (controllers, actions) that depends on a core module (services, repositories, domain model). Analysing only one module at a time truncates call chains at the module boundary. Pass both as --source roots to get end-to-end paths:
java -jar carve.jar \
--source api:/path/to/myapp-api/src/main/java \
--source core:/path/to/myapp-core/src/main/java \
--classpath /path/to/lib/spring-webmvc-6.1.jar:/path/to/lib/spring-tx-6.1.jar \
--markers my-markers.properties \
--print-risks --print-paths \
--output ./reportsBoth modules are parsed from source so cross-module type references resolve fully — no JAR for either module is needed on --classpath. The framework JARs are still useful for resolving Spring annotations and interface hierarchies.
In multi-project mode each node is prefixed with its project name, so cross-module call chains are immediately visible:
Risk 1/1 [HTTP]
──────────────────────────────────────────────────────────────
[api] DocumentController.upload() @Transactional
└─► [core] SigningService.sign() @Transactional
└─► [core] RemoteSigner.call() ← HTTP
With --print-paths the 10 longest call chains are printed:
Longest call chains: 10
──────────────────────────────────────────────────────────────
Path 1/10 (depth: 8)
──────────────────────────────────────────────────────────────
OrderService.checkout() @Transactional
└─► OrderServiceImpl.process()
└─► OrderServiceImpl.finalizeOrder() @Transactional
└─► PaymentClient.charge()
...
The paths include annotations (@Transactional) and external-call markers ([HTTP], [JDBC]) on each node where relevant. Results are also included in analysis.json under the longestPaths key.
With --print-risks each detected risk is printed as an indented call stack:
Transaction risks: 1
──────────────────────────────────────────────────────────────
Risk 1/1 [HTTP]
──────────────────────────────────────────────────────────────
OrderService.checkout() @Transactional
└─► OrderServiceImpl.process()
└─► PaymentClient.charge() ← HTTP
SpringMarkers covers the standard Spring/JEE ecosystem, but some projects integrate vendor SDKs or custom RPC clients that are not recognised out of the box. Use a markers properties file to teach the analyser about them:
# analyzer-markers.properties
# Format: fully-qualified class name (or package prefix ending with '.') = CALL_TYPE
# CALL_TYPE values: HTTP, MESSAGING, JDBC, JPA, CACHE
# Payment gateway SDK — makes HTTPS calls to a remote endpoint
com.acme.payments. = HTTP
# Internal messaging library
com.acme.legacy.mq. = MESSAGINGA key ending with . matches the entire package subtree; otherwise the match is exact.
Auto-discovery: the analyser looks for analyzer-markers.properties automatically, first in the parent directory of the source root, then in the current working directory. The --markers flag overrides this and accepts any path.
Local marker files: the markers/ directory is gitignored — drop project-specific marker files there (they often reference proprietary vendor class names you don't want in version control) and point to them explicitly:
java -jar carve.jar src/main/java --markers markers/my-project.properties| File | Description |
|---|---|
class-graph.html |
Interactive 3D class viewer — open in a browser. One node per class; rotate/zoom/pan, per-project filtering, risk highlighting, and search. |
package-graph.html |
Interactive 3D package viewer — open in a browser. One node per package, sized by class count; optional "Group by project" clustering; modernisation hotspots (unstable hubs / extraction candidates / stable cores) colour-coded with highlight toggle and "only hotspots" filter. |
class-graph.gexf |
Class-level graph for Gephi — colour/size pre-set, every attribute (project, transactional, external, cyclic, inRisk, methods) available for partitioning and filtering. Edges carry edgeKind, chaWeight and implFanOut, the last as a numeric column so a Gephi range filter isolates the inferred couplings that are genuinely ambiguous. |
call-graph.dot |
Graphviz DOT (method-level), opt-in via --dot — colour-coded by role (yellow = @Transactional, red = external call, orange = both). On large codebases the rendered SVG is an unreadable hairball; use the interactive exports instead. |
class-edges.csv |
Class-level edges, one row per coupling — source,sourceProject,target,targetProject,weight,chaWeight,implFanOut,edgeKind. The compact, greppable form of the class graph: edgeKind=cha marks an inferred edge, and implFanOut says how many implementations that inference was choosing between — 1 is exact, >1 is a guess worth checking (details). |
analysis.json |
JSON report: graph summary, transaction risks, longest paths, cyclic clusters, package coupling (flat profile + modernisation hotspots), lock risks. |
Interactive exploration (recommended). A method-level graph of a large monolith (10k+ nodes) is an unreadable hairball as a static SVG. The tool therefore collapses to class level and package level, each emitted as a self-contained 3D viewer:
class-graph.html— self-contained 3D viewer (WebGL), nothing to install; just open it.package-graph.html— same viewer at package granularity; includes a "Group by project" toggle that clusters packages by module, plus colour-coded modernisation hotspots with a "Highlight hotspots" toggle and an "only hotspots" filter.class-graph.gexf— load into Gephi for ForceAtlas2 layout, community detection, and rich attribute filtering.
class-graph.html — classes coloured by package, risks highlighted in red
package-graph.html — packages sized by class count, grouped by project
The static method-level DOT is opt-in (--dot), mainly useful on small modules:
java -jar carve.jar src/main/java --dot
dot -Tsvg reports/call-graph.dot -o reports/call-graph.svgSee AI.md for the full guide — which report answers which question, the practices that turned out to be worth the tokens, one that was not, and the caveats to pass along.
The reports work as grounding input for an LLM planning a modernisation, because the numbers are deterministic and reproducible: the same source tree yields the same call graph, the same risk paths, the same coupling metrics. An assistant working from source alone has to guess at the call structure, and guesses drift between runs. class-edges.csv is the one to hand to a model — the whole class graph, one row per coupling, at a fraction of the GEXF's bytes.
The highest-value use is asking for the why rather than the what: carve supplies topology the assistant cannot reliably derive, and the assistant supplies intent the topology cannot express. A package flagged as an unstable hub may turn out to be a Strategy pattern with the seam already cut — which reverses its position in the extraction order.
Java version support: source parsing tracks the Java version supported by the bundled Spoon/ECJ (currently up to Java 25). A brand-new Java release may need a Spoon upgrade before its newest syntax is recognised.
Spring AOP self-invocation: @Transactional only takes effect on public methods called through the Spring proxy. A method calling this.otherMethod() bypasses the proxy and its annotation. The analyser cannot detect self-invocations and conservatively treats all calls as if the annotation is honoured.
Dynamic dispatch: interface calls within the analysed source tree are resolved via CHA (see above). Calls that cross the source boundary — third-party interfaces, dynamic proxies, Spring AOP advice, or reflection — are not tracked.
- JDK 25 (other versions ≥ 21 work for compilation, but Spoon's Java 25 source support requires the matching toolchain)
- No separate Gradle installation — the wrapper (
./gradlew) downloads the right version automatically
./gradlew test # compile + run all tests
./gradlew check # tests + coverage gate (fails if coverage regresses)
./gradlew shadowJar # build fat-jar → build/libs/carve-<version>.jar
./gradlew build # both of the abovecheck enforces a JaCoCo coverage floor (90% instructions, 80% branches); a
change that drops below it fails the build.
When built locally the JAR is versioned dev-SNAPSHOT. Pass VERSION=x.y.z to produce a named build:
VERSION=1.2.0 ./gradlew shadowJar
# → build/libs/carve-1.2.0.jarReleases are created by pushing a version tag. GitHub Actions builds the fat-jar and attaches it to the release automatically:
git tag v1.0.0
git push origin v1.0.0The tag must follow the v<semver> format (e.g. v1.0.0, v1.2.3). The resulting JAR and release notes are published on the Releases page.
The code under src/main/java/com/codingful/carve/ is organised into packages that
mirror the analysis pipeline:
| Package | Responsibility |
|---|---|
spring |
Spring/Jakarta FQN constants and stereotype/annotation detection |
model |
Call-graph vertex (MethodNode) and its enums (component type, external-call type, propagation) |
extractor |
Spoon CtScanner that builds the graph and resolves calls (CHA), plus project and custom-marker resolution |
graph |
JGraphT wrapper with filtered views over the call graph |
analyzer |
Transaction-risk, longest-path, coupling (SCC + metrics) and DB-lock-risk analyses |
reporter |
Class/package graph collapse and the HTML, GEXF, DOT, JSON and console exporters |
util |
Shared FQN string helpers |
The top-level classes wire it together: Carve (CLI entry point + pipeline
orchestration), CarveConfig (argument parsing and validation), ReportWriter
(parallel report generation) and Analyses (aggregated results).
To add detection for a well-known library (e.g. a gRPC client), add its FQN to SpringMarkers.java. For project-specific vendor SDKs, use a --markers file instead — this keeps SpringMarkers focused on standard library patterns.
Supported data-access libraries
| Library | Classes detected | Tagged as |
|---|---|---|
| Spring JDBC | JdbcTemplate, NamedParameterJdbcTemplate |
JDBC |
| MyBatis | SqlSession, SqlSessionTemplate |
JDBC |
| JPA / Hibernate | EntityManager (javax + jakarta) |
JPA |
| Spring Data | JpaRepository, CrudRepository, … |
JPA |
Built on Spoon for Java source parsing and type resolution, and JGraphT for the call-graph and graph algorithms.
AGPL 3.0, compatible with the licenses of its bundled dependencies. Their attributions and licenses are listed in THIRD-PARTY-NOTICES.md (also shipped inside the JAR).