<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[ArchVerity Engineering]]></title><description><![CDATA[ArchVerity Engineering]]></description><link>https://archverity.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>ArchVerity Engineering</title><link>https://archverity.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Mon, 21 Sep 2026 17:23:25 GMT</lastBuildDate><atom:link href="https://archverity.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Why Spring Contracts Break Between Repositories While Everything Still Compiles]]></title><description><![CDATA[Spring contracts often fail at repository boundaries even when every service compiles independently. Detecting that drift early requires modeling relationships, preserving source and configuration evi]]></description><link>https://archverity.hashnode.dev/why-spring-contracts-break-between-repositories-while-everything-still-compiles</link><guid isPermaLink="true">https://archverity.hashnode.dev/why-spring-contracts-break-between-repositories-while-everything-still-compiles</guid><category><![CDATA[spring-boot]]></category><category><![CDATA[Java]]></category><category><![CDATA[kafka]]></category><category><![CDATA[Microservices]]></category><category><![CDATA[software architecture]]></category><dc:creator><![CDATA[Pavlo Putrenkov]]></dc:creator><pubDate>Sat, 19 Sep 2026 23:08:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6aaf1436f0622c962470f33e/4baa340e-424d-4655-b5ca-d53bfe609f5a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Spring contracts often fail at repository boundaries even when every service compiles independently. Detecting that drift early requires modeling relationships, preserving source and configuration evidence, and distinguishing proven mismatches from unknowns. This article applies that model to OpenFeign, Spring MVC, Kafka, DTOs, baselines, and CI.</p>
<p>Consider a client in one repository:</p>
<pre><code class="language-java">@FeignClient(name = "payments")
interface PaymentClient {
    @PostMapping("/payments/{id}")
    PaymentDto update(
        @PathVariable String id,
        @RequestBody PaymentDto dto
    );
}
</code></pre>
<p>And the provider in another:</p>
<pre><code class="language-java">@RestController
class PaymentController {
    @PutMapping("/payments/{id}")
    PaymentDto update(
        @PathVariable String id,
        @RequestBody PaymentDto dto
    ) {
        // ...
    }
}
</code></pre>
<p>Both fragments are valid Java. Both are valid Spring. The path and DTO even appear to agree. But the relation is broken: the caller sends <code>POST</code>, while the provider accepts <code>PUT</code>.</p>
<p>The compiler cannot report this because the contract does not live in either repository. It lives <strong>between</strong> them.</p>
<p>This article develops a practical model for detecting that kind of drift before runtime. The same model applies to OpenFeign and Spring MVC, Kafka producers and consumers, retry/DLT chains, and external OpenAPI or AsyncAPI manifests.</p>
<blockquote>
<p><strong>Disclosure:</strong> I build <a href="https://plugins.jetbrains.com/plugin/34234-archverity">ArchVerity</a>, a paid IntelliJ IDEA plugin with a 14-day trial. The product grew out of the evidence model described here. The examples below are intentionally small and synthetic; no customer incident or measured ROI is implied.</p>
</blockquote>
<h2>The real unit of analysis is a relationship</h2>
<p>Most static analysis starts with a file, symbol, or module. Integration defects need a different starting point: a relationship.</p>
<p>For HTTP, the useful relation is not simply "this method has <code>@GetMapping</code>." It is closer to:</p>
<pre><code class="language-text">client method
  -&gt; resolved service identity
  -&gt; HTTP method + normalized path
  -&gt; provider endpoint
  -&gt; request/response contract
</code></pre>
<p>For Kafka, it is not simply "this method sends a message." The flow may be:</p>
<pre><code class="language-text">producer
  -&gt; resolved topic
  -&gt; consumer group
  -&gt; retry topic
  -&gt; dead-letter topic
</code></pre>
<p>Each node can be valid in isolation. The failure appears only after the nodes are connected.</p>
<p>That leads to a useful separation of concerns:</p>
<ol>
<li><strong>Relation</strong>: which two or more elements are expected to work together?</li>
<li><strong>Evidence</strong>: which source locations and configuration values support that relation?</li>
<li><strong>Finding</strong>: what exactly disagrees, and how confident are we?</li>
</ol>
<p>Keeping these concepts separate matters. A finding without evidence becomes another warning that developers learn to ignore. Evidence without a relation is just an inventory. A relation without an explicit finding cannot explain what action is required.</p>
<h2>Use three states, not two</h2>
<p>A contract analyzer is tempting to model as a Boolean check: match or mismatch. Real repositories need a third state.</p>
<ul>
<li><code>MATCH</code>: both sides were found and the supported comparison agrees.</li>
<li><code>MISMATCH</code>: both sides were found and a supported comparison disagrees.</li>
<li><code>UNKNOWN</code>: the available source, configuration, or analysis scope is insufficient.</li>
</ul>
<p><code>UNKNOWN</code> is not a softer version of success. It is also not automatically a defect.</p>
<p>Suppose a Kafka producer resolves <code>orders.events.v2</code>, but no consumer is found. Several explanations are possible:</p>
<ul>
<li>the topic is orphaned;</li>
<li>the consumer is in a repository that was not scanned;</li>
<li>the consumer is implemented by a third-party system;</li>
<li>the topic name is transformed at runtime;</li>
<li>the relationship is declared in an external manifest that was not provided.</li>
</ul>
<p>Reporting a high-confidence error would overstate the evidence. Reporting success would hide risk. The honest result is <code>UNKNOWN</code>, accompanied by the scope and the point where resolution stopped.</p>
<p>This distinction is one of the most important ways to keep static analysis trustworthy. A smaller set of explainable findings is usually more valuable than a large issue count built on guesses.</p>
<h2>HTTP matching needs normalization and provenance</h2>
<p>A minimal HTTP relation key usually combines:</p>
<pre><code class="language-text">service identity
  + normalized HTTP method
  + normalized path template
  + request/response evidence
</code></pre>
<p>Path normalization should recognize that <code>/orders/{id}</code> and <code>/orders/{orderId}</code> have the same structural position. It should not erase real differences such as <code>/orders/{id}</code> versus <code>/v2/orders/{id}</code>.</p>
<p>The same principle applies to class-level and method-level mappings. A provider may compose its route like this:</p>
<pre><code class="language-java">@RequestMapping("/v2/orders")
class OrdersController {
    @GetMapping("/{id}")
    OrderResponse get(@PathVariable UUID id) {
        // ...
    }
}
</code></pre>
<p>The effective route is <code>/v2/orders/{id}</code>, so comparing only the method annotation would be wrong.</p>
<p>Configuration introduces another layer. A Feign client may get its service identity or base URL from properties, profiles, or environment placeholders. A useful result should therefore preserve provenance:</p>
<ul>
<li>the annotation that declared the client;</li>
<li>the property key that supplied a value;</li>
<li>the profile or manifest from which the value came;</li>
<li>the provider mapping that was selected;</li>
<li>the normalization steps used to compare them.</li>
</ul>
<p>The analyzer should be able to open both sides. If a developer still has to search manually for the provider after seeing a client-side warning, much of the context has been lost.</p>
<h2>DTO names are not contracts</h2>
<p>It is easy to stop after matching an endpoint and a Java class name. That is not enough.</p>
<p>These two DTOs share a name but not a wire contract:</p>
<pre><code class="language-java">record PaymentDto(
    String id,
    BigDecimal amount,
    String currency
) {}
</code></pre>
<pre><code class="language-java">record PaymentDto(
    String id,
    BigDecimal amount,
    @JsonProperty("currency_code") String currency
) {}
</code></pre>
<p>A useful DTO comparison may include field names, serialized names, requiredness, collections, nullability, and nested shapes. Even then, the supported subset must be explicit. Jackson mix-ins, custom serializers, polymorphic hierarchies, reflection, and generated classes can move the effective contract beyond what source analysis can prove.</p>
<p>The correct response is not to pretend that every serialization rule has been modeled. It is to identify the evidence that was compared and surface uncertainty where the model ends.</p>
<h2>Kafka requires a flow, not a topic search</h2>
<p>Matching a producer and consumer by a string literal handles only the easiest case.</p>
<p>In a Spring system, the topic may come from a constant:</p>
<pre><code class="language-java">kafkaTemplate.send(Topics.ORDER_EVENTS, event);
</code></pre>
<p>The consumer may resolve a placeholder:</p>
<pre><code class="language-java">@KafkaListener(
    topics = "${orders.events.topic}",
    groupId = "${orders.events.group}"
)
void consume(OrderEvent event) {
    // ...
}
</code></pre>
<p>Retry and DLT behavior may be declared elsewhere:</p>
<pre><code class="language-java">@RetryableTopic(
    attempts = "3",
    dltTopicSuffix = "-dlt"
)
</code></pre>
<p>The relation must preserve the steps by which values were resolved. When a profile changes the topic, the finding should identify that profile. When a placeholder cannot be resolved, the result should become <code>UNKNOWN</code> instead of silently substituting a guessed value.</p>
<p>This is also why scanning a single repository can be misleading. A topology view is useful only when its scope is visible. "No consumer in the analyzed repositories" is a defensible statement. "This topic has no consumer" may not be.</p>
<h2>Baselines turn a scan into a delivery control</h2>
<p>A full scan answers: "What does the system look like now?"</p>
<p>Code review usually asks a narrower question: "What changed compared with the accepted state?"</p>
<p>A deterministic baseline makes that comparison possible. A subsequent scan can report:</p>
<ul>
<li>a new or removed contract;</li>
<li>a changed caller/provider relation;</li>
<li>a new or resolved finding;</li>
<li>a confidence change;</li>
<li>a new <code>UNKNOWN</code> caused by reduced analysis scope.</li>
</ul>
<p>This is much more actionable than sending a permanent red list into every pull request. Existing debt can stay visible without blocking unrelated work, while newly introduced drift becomes reviewable.</p>
<p>The same result can be represented for different consumers:</p>
<ul>
<li>an IDE view for navigation and investigation;</li>
<li>JSON for automation;</li>
<li>SARIF for CI and code-review systems;</li>
<li>an SVG topology for a human-readable architecture snapshot.</li>
</ul>
<p>The important property is determinism. If the same source and configuration produce unstable identifiers or ordering, every baseline becomes noisy.</p>
<h2>Static evidence complements runtime tests</h2>
<p>This approach does not replace integration tests, consumer-driven contract tests, schema registries, OpenAPI, or AsyncAPI.</p>
<p>Each tool answers a different question:</p>
<ul>
<li>static evidence asks what relationships can be derived from the current source and configuration;</li>
<li>specifications describe an intended external contract, when maintained as a source of truth;</li>
<li>contract tests verify selected producer/consumer expectations;</li>
<li>integration tests exercise behavior in a running environment;</li>
<li>observability shows what happened in production.</li>
</ul>
<p>Static analysis is useful because it can produce an early, local signal while a developer still has the change in context. Runtime checks remain necessary because routing, serialization, service meshes, gateways, and environment-specific behavior may change the effective contract.</p>
<h2>Where the model should stop</h2>
<p>There are cases where a precise answer cannot be justified:</p>
<ul>
<li>URLs or topics are assembled by arbitrary runtime code;</li>
<li>custom serialization changes the payload outside the visible type model;</li>
<li>consumers live outside the available repositories and no manifest describes them;</li>
<li>a gateway or service mesh rewrites routes at runtime;</li>
<li>reflection or generated code leaves insufficient source evidence.</li>
</ul>
<p>In these situations, a bounded diagnostic is better than a confident-looking graph. The quality of an architecture tool is partly determined by how clearly it marks the edge of its knowledge.</p>
<h2>From model to workflow</h2>
<p>The practical workflow I use is:</p>
<pre><code class="language-text">detect -&gt; prove -&gt; gate
</code></pre>
<ol>
<li><strong>Detect</strong> a changed HTTP or Kafka relation.</li>
<li><strong>Prove</strong> it with caller/provider or producer/consumer source evidence.</li>
<li><strong>Gate</strong> only the relevant change using a baseline and deterministic output.</li>
</ol>
<p>I implemented this workflow in ArchVerity 2.0.2 for IntelliJ IDEA. The current release performs analysis locally in the IDE and does not send project source, architecture snapshots, findings, or usage telemetry to the publisher. It supports Spring MVC, OpenFeign/HTTP clients, Kafka producer/consumer/retry/DLT evidence, baseline diff, and JSON/SVG/SARIF outputs.</p>
<p>If cross-repository Spring contracts are a problem in your team, you can try it on one real flow from the <a href="https://plugins.jetbrains.com/plugin/34234-archverity">JetBrains Marketplace</a>. I am especially interested in three questions:</p>
<ol>
<li>What do you treat as sufficient evidence that a client and provider belong to the same contract?</li>
<li>Which cases should be <code>MISMATCH</code>, and which should remain <code>UNKNOWN</code>?</li>
<li>Is your main pain HTTP drift, Kafka topology, or getting a reviewable baseline into CI?</li>
</ol>
<hr />
<p><em>Editorial note: AI assistance was used for drafting and structure. The technical examples and product claims were checked against the current product documentation.</em></p>
]]></content:encoded></item></channel></rss>