<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Maksim Matlakhov</title>
    <description>The latest articles on Forem by Maksim Matlakhov (@maksim_matlakhov).</description>
    <link>https://forem.com/maksim_matlakhov</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3074543%2Fa2ceffc9-d02d-479f-95dd-e6d201a12aa5.jpg</url>
      <title>Forem: Maksim Matlakhov</title>
      <link>https://forem.com/maksim_matlakhov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/maksim_matlakhov"/>
    <language>en</language>
    <item>
      <title>Event Modeling: Visible Issues and My Vision</title>
      <dc:creator>Maksim Matlakhov</dc:creator>
      <pubDate>Thu, 23 Oct 2025 09:35:51 +0000</pubDate>
      <link>https://forem.com/maksim_matlakhov/event-modeling-visible-issues-and-my-vision-34en</link>
      <guid>https://forem.com/maksim_matlakhov/event-modeling-visible-issues-and-my-vision-34en</guid>
      <description>&lt;p&gt;During my journey learning Event Modeling as part of my VibeTDD framework exploration, I discovered some issues that from my point of view limit the framework's adoption and effectiveness. After extensive research and community discussions, I decided to build a proof-of-concept that addresses these core problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problems I Discovered
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Canvas Layout Nightmare
&lt;/h3&gt;

&lt;p&gt;Event Modeling relies heavily on visual canvases like Miro, creating constant friction during the modeling process. The specific pain points include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Continuous resizing and repositioning&lt;/strong&gt;: Every addition requires manual layout adjustments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cascading layout changes&lt;/strong&gt;: Adding one element forces you to resize multiple components&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale anxiety&lt;/strong&gt;: Managing hundreds of slices with multiple scenarios becomes overwhelming&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time waste&lt;/strong&gt;: Significant effort spent on layout management instead of actual modeling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I shared this frustration on LinkedIn, and multiple Event Modeling practitioners confirmed these concerns, including the framework's creator and experienced practitioners who acknowledged the tooling limitations.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Business Rules Documentation Gap
&lt;/h3&gt;

&lt;p&gt;My investigation revealed a critical architectural problem: &lt;strong&gt;Event Modeling has no standardized way to handle business rules&lt;/strong&gt;. Looking at examples, I couldn't find clear documentation for rules like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Max 3 items in shopping cart&lt;/li&gt;
&lt;li&gt;Single payout limit: 30&lt;/li&gt;
&lt;li&gt;Total payouts can't exceed 200&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates several problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Information fragmentation&lt;/strong&gt;: Rules must live "somewhere else" (Confluence, Jira, comments)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incomplete model slices&lt;/strong&gt;: Developers need to "hunt for details" across multiple systems&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Synchronization risk&lt;/strong&gt;: Miro boards and documentation can drift apart&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code generation limitations&lt;/strong&gt;: Yes, it's possible to export the model as json, but having incomplete info I mentioned above, code generation can be tricky&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even after consulting with industry experts, I found that there's no standard solution for this need.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Simple POC to Test My Ideas
&lt;/h2&gt;

&lt;p&gt;To explore solutions to these issues, I built a simple proof-of-concept - a quick, trivial implementation to test whether a structured approach could work. This is just the beginning, a basic prototype to validate the core concepts rather than a complete tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Slices Board
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frjwi1iqbr6fxmynx903x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frjwi1iqbr6fxmynx903x.png" alt="Event Modeling Board Overview"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The main board shows slices in a clean, structured format without manual positioning. Each slice contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Command&lt;/strong&gt; (blue): The action being performed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event&lt;/strong&gt; (orange): The result of the action&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test Scenarios&lt;/strong&gt;: Direct link to business logic validation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Structured Slice Editor
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fycyxjpxx88exs60atrma.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fycyxjpxx88exs60atrma.png" alt="Slice Editor Interface"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The slice editor provides structured forms for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model selection&lt;/strong&gt;: Choose a domain object&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operation/Status&lt;/strong&gt;: Define the action and the slice status&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Command definition&lt;/strong&gt;: Specify the command name and parameters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Field management&lt;/strong&gt;: Add fields with proper types and constraints&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event definition&lt;/strong&gt;: Define the resulting event and its fields&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rules section&lt;/strong&gt;: Add and manage business rules manually or with AI generation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Smart Field Connections
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7efi5v5spa3a2158dlm1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7efi5v5spa3a2158dlm1.png" alt="Connect Field to Event"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The system supports dependency management between objects. When creating an order with &lt;code&gt;userId&lt;/code&gt; and &lt;code&gt;courseId&lt;/code&gt;, we can connect these fields to their respective events by selecting from existing events in the system.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Custom Type System
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffgnxk6svdqlzky0i9heq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffgnxk6svdqlzky0i9heq.png" alt="Edit Custom Type"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Beyond basic types (String, Date, Long etc), the system supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Common types&lt;/strong&gt; like Email, CountryCode, CurrencyCode&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom complex types&lt;/strong&gt;: Define reusable business objects like &lt;code&gt;Price&lt;/code&gt; with structured fields&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type validation&lt;/strong&gt;: Ensure consistency across the model&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Test Scenarios Generation
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvyo9wxzomhkk1plsoxu3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvyo9wxzomhkk1plsoxu3.png" alt="Generated Test Scenarios"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The system generates test scenarios based on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Configuration parameters&lt;/strong&gt;: Business rules with configurable limits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Existence rules&lt;/strong&gt;: Validation that dependent objects exist&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business logic&lt;/strong&gt;: Custom rules with success/failure cases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual definition&lt;/strong&gt;: Direct scenario creation through the interface&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Check the full demo video:
&lt;/h3&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/m7xJnHm_P0o"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Advantages of This Approach
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;No Layout Management&lt;/strong&gt;&lt;br&gt;
Eliminates the constant resizing, moving, and adapting that plagues canvas-based tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structured Business Rules&lt;/strong&gt;&lt;br&gt;
Rules are first-class citizens with clear categorization, configuration options, and automatic test generation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Type Safety&lt;/strong&gt;&lt;br&gt;
Proper type system prevents inconsistencies and enables better code generation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dependency Management&lt;/strong&gt;&lt;br&gt;
Manual connection of object relationships with clear visualization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Export Capability&lt;/strong&gt;&lt;br&gt;
Complete model data can be exported for code generation without external documentation hunting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Picture
&lt;/h2&gt;

&lt;p&gt;This proof-of-concept addresses what I call the "notepad problem" of Event Modeling. As I mentioned in &lt;a href="https://www.linkedin.com/posts/maksim-matlakhov_eventmodeling-activity-7384160669052428288-DJEf" rel="noopener noreferrer"&gt;the LinkedIn discussion&lt;/a&gt;: &lt;em&gt;"I can imagine what would happen if JetBrains didn't provide a tool for Kotlin and suggested devs write code in notepad 🙂"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Event Modeling is a powerful framework, but poor tooling creates unnecessary friction that slows adoption and execution. By treating Event Modeling as structured data rather than free-form drawings, we can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Accelerate modeling sessions&lt;/strong&gt; by removing layout management overhead&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ensure completeness&lt;/strong&gt; by requiring all necessary information upfront&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable automation&lt;/strong&gt; through structured data that AI can understand easily&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improve maintainability&lt;/strong&gt; by keeping rules and scenarios synchronized&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale effectively&lt;/strong&gt; without canvas limitations&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Technical Implementation Notes
&lt;/h2&gt;

&lt;p&gt;The POC is built with modern web technologies and demonstrates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Form-based interfaces&lt;/strong&gt; that are faster than drag-and-drop&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intelligent defaults&lt;/strong&gt; that reduce repetitive data entry&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validation at input time&lt;/strong&gt; rather than discovery later&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clear separation&lt;/strong&gt; between business rules and technical implementation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extensible type system&lt;/strong&gt; for domain-specific needs&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;This proof-of-concept establishes a crucial foundation: &lt;strong&gt;structured slice data ready for code generation and dynamic test execution&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I've conducted a quick experiment that demonstrates the system can execute tests stored in the database rather than generating static test files. This means the test scenarios visible on the board become the source of truth, no code generation step required, no synchronization issues between specifications and tests.&lt;/p&gt;

&lt;p&gt;The implications are significant: changes to business rules or test scenarios immediately affect test execution without any build or deployment steps. This creates a truly dynamic testing environment where the Event Model directly drives validation.&lt;/p&gt;

</description>
      <category>eventmodeling</category>
      <category>architecture</category>
      <category>softwaredevelopment</category>
      <category>eventdriven</category>
    </item>
    <item>
      <title>Event Handling: Automatic Event Bootstrapping</title>
      <dc:creator>Maksim Matlakhov</dc:creator>
      <pubDate>Mon, 06 Oct 2025 14:44:32 +0000</pubDate>
      <link>https://forem.com/maksim_matlakhov/event-handling-automatic-event-bootstrapping-5epl</link>
      <guid>https://forem.com/maksim_matlakhov/event-handling-automatic-event-bootstrapping-5epl</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This post continues the event handling series. Check out the &lt;a href="https://dev.to/maksim_matlakhov/event-handling-inbox-pattern-for-complex-scenarios-5b83"&gt;previous post on Inbox Pattern&lt;/a&gt; to learn how we handle event ordering and idempotency.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Problem with Historical Events
&lt;/h2&gt;

&lt;p&gt;When you create a new service that subscribes to events from other domains, you face a common challenge: &lt;strong&gt;your service needs historical data to function independently&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Imagine you're building an Order Service that needs to display user information. You subscribe to user events like &lt;code&gt;UserCreated&lt;/code&gt;, &lt;code&gt;PersonalDataUpdated&lt;/code&gt;, and &lt;code&gt;StatusUpdated&lt;/code&gt;. But what about users who were created before your service existed? Your database is empty, and you can't show any order details properly.&lt;/p&gt;

&lt;p&gt;This applies whether your services are deployed separately or as part of a modular monolith. The key is that each service owns its data and communicates through events.&lt;/p&gt;

&lt;h3&gt;
  
  
  Some Approaches to Manage That
&lt;/h3&gt;

&lt;p&gt;Teams typically handle this in a few ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Manual data migration&lt;/strong&gt; - Write custom scripts to copy data from the source service's database. This breaks service boundaries and creates tight coupling.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Batch import endpoints&lt;/strong&gt; - Ask the source team to build special APIs for bulk data export. This requires coordination and extra development work.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Accept the gap&lt;/strong&gt; - Start fresh and only handle new events. This means your service is incomplete until enough time passes.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All of these approaches are painful and error-prone.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Better Way: Automatic Event Bootstrap
&lt;/h2&gt;

&lt;p&gt;The solution is surprisingly elegant: &lt;strong&gt;fetch historical events the same way you'll receive future events&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's the key insight: if every service provides a simple method to fetch events by topic and timestamp, any dependent service can automatically bootstrap itself during startup.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;p&gt;The VT framework provides all the infrastructure for event bootstrapping. On the service side, you only need two simple steps:&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1: Create a Storage Adapter
&lt;/h4&gt;

&lt;p&gt;Create a simple storage adapter for tracking bootstrap progress:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderEventBootstrapStorageAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;OrderEventBootstrapRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;VTMongoEventBootstrapStorageAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The adapter extends the framework's base class and handles saving/loading bootstrap records. You can use any database - MongoDB, PostgreSQL, etc.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Add the EventBootstrap Annotation
&lt;/h4&gt;

&lt;p&gt;Add the annotation to your consumer class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@EventConsumer&lt;/span&gt;
&lt;span class="nd"&gt;@EventBootstrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;consumerName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"UserView"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// define a static unique name&lt;/span&gt;
    &lt;span class="n"&gt;storageBean&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OrderEventBootstrapStorageAdapter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// use defined adapter&lt;/span&gt;
    &lt;span class="n"&gt;clientBean&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UserEventClientV1&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="c1"&gt;// each service provides an event client&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserEventsConsumerV1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;orchestrator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UserViewOrchestrator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onCreated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventV1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserCreatedV1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;orchestrator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onPersonalDataUpdated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventV1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PersonalDataUpdatedV1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;orchestrator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onStatusUpdated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventV1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserStatusUpdatedV1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;orchestrator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! No special bootstrap code needed. The framework handles everything else.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the Framework Does
&lt;/h3&gt;

&lt;p&gt;The VT framework provides the complete bootstrapping infrastructure.&lt;/p&gt;

&lt;h4&gt;
  
  
  Event Fetching Interface
&lt;/h4&gt;

&lt;p&gt;The framework defines a standard interface that each domain service implements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;VTEventClientV1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventsFetchRequestV1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;BrokerMessageV1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;EventsFetchRequestV1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventsFetchRequestBodyV1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;EventsFetchRequestBodyV1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;topics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;itemsCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can be implemented using internal clients (for modular monoliths or testing), REST APIs, WebSockets, or any other protocol. The important part is the consistent interface.&lt;/p&gt;

&lt;h4&gt;
  
  
  Automatic Topic Detection
&lt;/h4&gt;

&lt;p&gt;The framework automatically discovers which topics your consumer needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;registerConsumer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;beanName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;registeredTopics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;MutableSet&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mutableSetOf&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;consumer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Detect event types from method parameters&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;eventBodyType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractEventType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;registeredTopics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topicMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;eventBodyType&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

        &lt;span class="n"&gt;consumerRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;eventType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;eventBodyType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;bootstrapRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;registeredTopics&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Progressive Fetching with Retry Logic
&lt;/h4&gt;

&lt;p&gt;The bootstrap executor fetches events in batches with exponential backoff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;executeOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventBootstrapRecord&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;processedTotal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;hasMore&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;currentRecord&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hasMore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetchAndProcessBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentRecord&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;BatchResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Success&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;hasMore&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
                    &lt;span class="nf"&gt;markAsDone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentRecord&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;processedTotal&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;
                    &lt;span class="n"&gt;currentRecord&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;updateTimestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentRecord&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="s"&gt;"Processed batch of ${result.messages.size} events. "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
                        &lt;span class="s"&gt;"Total: $processedTotal"&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;BatchResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Failure&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;markAsFailed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentRecord&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;hasMore&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Progress Tracking
&lt;/h4&gt;

&lt;p&gt;The system tracks progress in a bootstrap collection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3d88ca1e-c166-4870-87fa-54ddd4b8cd77"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"consumerName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UserView"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DONE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"topics"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"user.model.created.v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"user.status.updated.v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"user.personal-data.updated.v1"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lastCreatedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-10-05T12:39:22.027Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"processedCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means you can safely stop and restart your service during bootstrap. It will resume from where it left off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same Handlers for Historical and Real-Time Events
&lt;/h2&gt;

&lt;p&gt;Here's the beautiful part: &lt;strong&gt;historical events use the exact same consumer methods as real-time events&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;callConsumers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BrokerMessageV1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventBootstrapRecord&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;consumerRegistry&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getConsumers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bootstrapConsumerName&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consumerName&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your consumers already handle event ordering and idempotency properly (see &lt;a href="https://dev.to/maksim_matlakhov/event-handling-keep-it-fast-and-simple-5867"&gt;the previous post&lt;/a&gt; for details), so receiving historical and real-time events simultaneously isn't a problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding New Features Later
&lt;/h2&gt;

&lt;p&gt;Let's say your Order Service initially subscribed to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;product.created.v1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;product.price.updated.v1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Later, you need to add product status information to your order table. Simply add a new handler method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@EventConsumer&lt;/span&gt;
&lt;span class="nd"&gt;@EventBootstrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;consumerName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ProductView"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;storageBean&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OrderEventBootstrapStorageAdapter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;clientBean&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ProductEventClientV1&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductEventsConsumerV1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Existing handlers&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onCreated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventV1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ProductCreatedV1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onPriceUpdated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventV1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PriceUpdatedV1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// New handler - framework detects the new topic automatically&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onStatusUpdated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventV1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ProductStatusUpdatedV1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Your new logic here&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On next restart, the framework detects the new topic and fetches only those historical events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;createRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bootstrap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventBootstrap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;topics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;existingRecords&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bootstrap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consumerName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;allExistingTopics&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;existingRecords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;topics&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;toSet&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;newTopics&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;topics&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;allExistingTopics&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newTopics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;newRecord&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EventBootstrapRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;consumerName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bootstrap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consumerName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;topics&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;newTopics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;lastCreatedAt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EPOCH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EventBootstrapStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IN_PROGRESS&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newRecord&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Perfect for Continuous Integration
&lt;/h2&gt;

&lt;p&gt;This approach works excellently with continuous deployment:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;First iteration&lt;/strong&gt;: Create your service with initial event subscriptions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy&lt;/strong&gt;: The service starts, bootstraps historical events, and begins receiving real-time events&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Later iteration&lt;/strong&gt;: Add new event handlers for new features&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy again&lt;/strong&gt;: The service automatically fetches only the new historical events&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No manual migrations. No coordination meetings. No data import scripts. Just deploy and let the framework handle it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bootstrap States
&lt;/h2&gt;

&lt;p&gt;The system tracks four states:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EventBootstrapStatus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;PENDING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// Ready to start&lt;/span&gt;
    &lt;span class="nc"&gt;IN_PROGRESS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// Currently fetching&lt;/span&gt;
    &lt;span class="nc"&gt;DONE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;// Successfully completed&lt;/span&gt;
    &lt;span class="nc"&gt;FAILED&lt;/span&gt;        &lt;span class="c1"&gt;// Error occurred&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also control automatic startup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@EventBootstrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;consumerName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"UserView"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;storageBean&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OrderEventBootstrapStorageAdapter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;clientBean&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UserEventClientV1&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;autoStart&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;  &lt;span class="c1"&gt;// Start manually via endpoint&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Event bootstrapping solves the historical data problem elegantly by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reusing the same event handlers&lt;/strong&gt; for historical and real-time events&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatically detecting&lt;/strong&gt; which events your consumers need&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tracking progress&lt;/strong&gt; so restarts don't lose work&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handling failures&lt;/strong&gt; with retry logic and backoff&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supporting incremental changes&lt;/strong&gt; when adding new features&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Working seamlessly&lt;/strong&gt; in both modular monoliths (internal clients) and distributed architectures (remote clients)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result? Your services can truly be independent, with complete data from day one, and the ability to evolve without painful migrations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source Code
&lt;/h2&gt;

&lt;p&gt;Full implementation is available in the &lt;a href="https://gitlab.com/vibetdd/examples/be/kotlin/3-api-user-product-order" rel="noopener noreferrer"&gt;VibeTDD GitLab repository&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>eventdriven</category>
      <category>architecture</category>
      <category>kotlin</category>
      <category>backend</category>
    </item>
    <item>
      <title>Event Handling: Inbox Pattern for Complex Scenarios</title>
      <dc:creator>Maksim Matlakhov</dc:creator>
      <pubDate>Fri, 03 Oct 2025 12:48:51 +0000</pubDate>
      <link>https://forem.com/maksim_matlakhov/event-handling-inbox-pattern-for-complex-scenarios-5b83</link>
      <guid>https://forem.com/maksim_matlakhov/event-handling-inbox-pattern-for-complex-scenarios-5b83</guid>
      <description>&lt;p&gt;In my &lt;a href="https://dev.to/maksim_matlakhov/event-handling-keep-it-fast-and-simple-5867"&gt;previous post&lt;/a&gt;, I covered direct event handling for simple, stateless operations. Today, I'm diving into the &lt;strong&gt;Inbox Pattern&lt;/strong&gt; – a robust approach for scenarios where immediate processing isn't safe or when you need more sophisticated handling strategies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Inbox Pattern?
&lt;/h2&gt;

&lt;p&gt;Not all events can be processed immediately upon arrival. Some scenarios require careful, controlled processing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Complex calculations&lt;/strong&gt; – Long-running computations that might timeout&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document conversions&lt;/strong&gt; – Converting HTML to PDF, processing large files is unpredictable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External service calls&lt;/strong&gt; – Sending emails via providers like SendGrid or Mailgun&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple event aggregation&lt;/strong&gt; – Combining data from several events before processing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource-intensive operations&lt;/strong&gt; – Tasks that require significant CPU, memory, or I/O&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Problem with Immediate Processing
&lt;/h3&gt;

&lt;p&gt;Processing events directly in the consumer can lead to serious issues:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timeout Issues&lt;/strong&gt; – Complex operations may exceed the message broker's in-flight time limit, causing the broker to assume failure and redeliver the message, potentially creating duplicate processing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retry Control Loss&lt;/strong&gt; – When execution fails, you're at the mercy of the broker's retry policy. Most brokers use simple exponential backoff without fine-grained control over retry attempts, timing, or failure tracking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resource Exhaustion&lt;/strong&gt; – High event volumes can overwhelm your service if each event triggers expensive operations synchronously.&lt;/p&gt;

&lt;p&gt;The Inbox Pattern solves this by &lt;strong&gt;decoupling event reception from processing&lt;/strong&gt;: we persist the event first, then process it asynchronously with our logic that we control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Processing Types
&lt;/h2&gt;

&lt;p&gt;Based on my experience, I've identified three distinct processing patterns:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Single Event Processing
&lt;/h3&gt;

&lt;p&gt;Each event is processed independently without considering other events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; A user status changes to "SUSPICIOUS". The fraud detection system needs to review all active orders for that user, check payment methods, and potentially flag transactions – all independently of other user status changes. In each update event I put previous and current state, so it's easy to identify a change with a single event.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Sequential Processing
&lt;/h3&gt;

&lt;p&gt;Events for the same entity must be processed in order to maintain consistency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; Calculating user reward points based on purchase history. Events like PurchaseCompleted → RefundIssued → BonusApplied must be processed in sequence per user. The final points balance depends on processing these events in the exact order they occurred.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Batch Processing
&lt;/h3&gt;

&lt;p&gt;Multiple events are processed together for efficiency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; Generating daily financial reports by collecting all transaction events throughout the day and processing them together at midnight. This avoids recalculating totals, averages, and summaries after every single transaction.&lt;/p&gt;

&lt;p&gt;For this post, we'll focus on &lt;strong&gt;Single Event Processing&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo Architecture
&lt;/h2&gt;

&lt;p&gt;To demonstrate the Inbox Pattern, I've created a modular monolith with three services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;User Service&lt;/strong&gt; – Manages user accounts and status changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Product Service&lt;/strong&gt; – Handles product catalog&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Order Service&lt;/strong&gt; – Processes orders and subscribes to user and product events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Scenario:&lt;/strong&gt; When a user's status changes, the User Service emits an event. The Order Service subscribes to these events, stores them in an inbox, and processes fraud checks when status transitions involve "SUSPICIOUS".&lt;/p&gt;

&lt;p&gt;You can explore the complete implementation here: &lt;a href="https://gitlab.com/vibetdd/examples/be/kotlin/3-api-user-product-order" rel="noopener noreferrer"&gt;GitLab Repository&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How It All Works Together
&lt;/h2&gt;

&lt;p&gt;Let's walk through the complete flow before diving into the code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;User Service&lt;/strong&gt; changes a user's status to &lt;code&gt;SUSPICIOUS&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event is published&lt;/strong&gt; via message broker (Kafka, RabbitMQ, etc.). In our case I use internal Spring implementation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Order Service consumer&lt;/strong&gt; receives the event and stores it immediately in the inbox&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduler triggers&lt;/strong&gt; every 10 seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runner fetches&lt;/strong&gt; pending events from the inbox database&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handler processes&lt;/strong&gt; each event, performing fraud checks on the user's orders&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On success:&lt;/strong&gt; Event marked as &lt;code&gt;SENT&lt;/code&gt;, processing complete&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On failure:&lt;/strong&gt; Event scheduled for retry with exponential backoff&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry continues&lt;/strong&gt; until either success or max retry limit reached&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; The event consumer's only job is to store the event quickly and acknowledge it to the broker. All complex processing happens asynchronously, with full control over retries and failure handling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation: Fraud Detection Example
&lt;/h2&gt;

&lt;p&gt;Now let's see how this is implemented in code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Storing Events
&lt;/h3&gt;

&lt;p&gt;When an event arrives, we store it immediately with context defining how it should be processed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@EventConsumer&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserInboxEventsConsumerV1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;OrderInboxEventStorageAdapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onStatusUpdated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventV1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserStatusUpdatedV1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nc"&gt;InboxEventContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;single&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;InboxTopic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FRAUD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key points:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;InboxEventContext.single()&lt;/code&gt; marks this for single event processing&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;InboxTopic.FRAUD&lt;/code&gt; groups related events together&lt;/li&gt;
&lt;li&gt;The event is stored as-is – no processing happens here&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: Defining Event Context
&lt;/h3&gt;

&lt;p&gt;The context structure tells our system &lt;strong&gt;how&lt;/strong&gt; to process events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;InboxEventContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;processingType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ProcessingType&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;companion&lt;/span&gt; &lt;span class="k"&gt;object&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;single&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;InboxEventContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;topic&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;processingType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ProcessingType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SINGLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProcessingType&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;SINGLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;SEQUENTIAL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;BATCH&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each stored event contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;InboxEventData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;InboxEventContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventV1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="nc"&gt;EventDtoBody&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Notification&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Notification&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Tracking Processing State
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;Notification&lt;/code&gt; object tracks processing attempts and status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Notification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PENDING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;executeAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;failedReasons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FailedReason&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;PENDING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;SENT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;FAILED&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Built-in retry logic with exponential backoff:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nc"&gt;Notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFailure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;baseRetryIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Notification&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;limitExceeded&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;maxRetries&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;newAttempts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;nextExecuteAt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limitExceeded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;executeAt&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;plusSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseRetryIn&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;newAttempts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLong&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limitExceeded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FAILED&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nc"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PENDING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;newAttempts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;failedReasons&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;failedReasons&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="nc"&gt;FailedReason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;occurredAt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s"&gt;"Unknown error"&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;executeAt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nextExecuteAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Configuring the Scheduler
&lt;/h3&gt;

&lt;p&gt;Define &lt;strong&gt;when&lt;/strong&gt; and &lt;strong&gt;how often&lt;/strong&gt; to process events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@ConfigurationProperties&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"order.inbox.single"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;OrderSingleInboxEventProps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;baseRetryIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;executeEvery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Duration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ofSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;VTInboxEventProps&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Configuration breakdown:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pageSize: 20&lt;/code&gt; – Process 20 events per batch&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;maxRetries: 15&lt;/code&gt; – Retry failed events up to 15 times&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;baseRetryIn: 2&lt;/code&gt; – Start with 2-second delays, increasing exponentially&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;executeEvery: 10s&lt;/code&gt; – Check for new events every 10 seconds&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;topic: null&lt;/code&gt; – Process all SINGLE type events (or specify a topic for granular control)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 5: Setting Up Processing Infrastructure
&lt;/h3&gt;

&lt;p&gt;Wire everything together with Spring configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="nd"&gt;@EnableSchedulerLock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;defaultLockAtMostFor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"PT30S"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@EnableConfigurationProperties&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderSingleInboxEventProps&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderInboxEventConfig&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;orderSingleEventRunner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;orderInboxEventStorageAdapter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;OrderInboxEventStorageAdapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;orderInboxEventHandlers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;VTInboxEventHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;,&lt;/span&gt;
        &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;OrderSingleInboxEventProps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;VTSingleEventRunner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;repository&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orderInboxEventStorageAdapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;handlers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orderInboxEventHandlers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;props&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;orderSingleEventScheduler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;orderMongoTemplate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;MongoTemplate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;orderSingleEventRunner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;VTSingleEventRunner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;taskScheduler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TaskScheduler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;OrderSingleInboxEventProps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;VTEventScheduler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;lockProvider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MongoLockProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderMongoTemplate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;eventRunner&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orderSingleEventRunner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;taskScheduler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;taskScheduler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;props&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; &lt;code&gt;@EnableSchedulerLock&lt;/code&gt; prevents duplicate processing in distributed environments using &lt;a href="https://github.com/lukas-krecan/ShedLock" rel="noopener noreferrer"&gt;ShedLock&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: The Event Runner
&lt;/h3&gt;

&lt;p&gt;The runner fetches pending events and processes them in batches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;VTSingleEventRunner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;VTInboxEventStoragePort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;VTInboxEventHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;VTInboxEventProps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;VTEventRunner&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;log&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KotlinLogging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;InboxEventData&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findPendingByTopic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findPendingByType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ProcessingType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SINGLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;processOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNotEmpty&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;processOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;InboxEventData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Notification&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notification&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"Processing inbox event: ${event.id}, topic: ${event.context.topic}"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;handlers&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nd"&gt;@Suppress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"UNCHECKED_CAST"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;VTInboxEventHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EventDtoBody&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;).&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;EventV1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EventDtoBody&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="n"&gt;notification&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toSuccess&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"Failed to process event: ${event.id}"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;notification&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFailure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;baseRetryIn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;updateEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;updateEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;InboxEventData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Notification&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notification&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"Failed to update event: ${event.id}"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Processing flow:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetch pending events (by topic or processing type)&lt;/li&gt;
&lt;li&gt;Find the appropriate handler for each event's topic&lt;/li&gt;
&lt;li&gt;Execute the handler&lt;/li&gt;
&lt;li&gt;Mark as success or schedule retry on failure&lt;/li&gt;
&lt;li&gt;Continue until no pending events remain&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 7: The Scheduler
&lt;/h3&gt;

&lt;p&gt;The scheduler ensures regular processing with distributed locking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;VTEventScheduler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;eventRunner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;VTEventRunner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;taskScheduler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TaskScheduler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;VTInboxEventProps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;lockProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ExtensibleLockProvider&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ApplicationListener&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ContextRefreshedEvent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;schedulingLockProvider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SchedulingLockProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lockProvider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;lockName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"inbox-${props.topic ?: eventRunner::class.simpleName}"&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onApplicationEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ContextRefreshedEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;taskScheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scheduleWithFixedDelay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;processEvents&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executeEvery&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;processEvents&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;DefaultLockingTaskExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schedulingLockProvider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executeWithLock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nc"&gt;Runnable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;eventRunner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="nc"&gt;LockConfiguration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="nc"&gt;Instant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                    &lt;span class="n"&gt;lockName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;schedulingLockProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLockAtMostFor&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                    &lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ZERO&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 8: Creating the Handler
&lt;/h3&gt;

&lt;p&gt;Finally, implement the business logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FraudTopicHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;InboxTopic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FRAUD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;coreFactory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;OrderCoreFactory&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;VTInboxEventHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserStatusUpdatedV1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventV1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserStatusUpdatedV1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;runBlocking&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;coreFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fraudUseCase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toCommand&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;EventV1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserStatusUpdatedV1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="nf"&gt;toCommand&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ManageFraudCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;previousStatus&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;currentStatus&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Handler responsibilities:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Listen for events with topic &lt;code&gt;FRAUD&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Transform the event into a domain command&lt;/li&gt;
&lt;li&gt;Execute the business logic (fraud check in this case)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Benefits
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Reliability&lt;/strong&gt; – Events aren't lost if processing fails&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Observability&lt;/strong&gt; – Track processing attempts and failures&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Flexibility&lt;/strong&gt; – Configure retry behavior per use case&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Scalability&lt;/strong&gt; – Distributed locks prevent duplicate processing&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Separation of Concerns&lt;/strong&gt; – Event reception and processing are independent&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Timeout Protection&lt;/strong&gt; – Long-running operations don't block message consumers&lt;/p&gt;

&lt;h2&gt;
  
  
  Coming Next
&lt;/h2&gt;

&lt;p&gt;In the next post, we'll tackle a critical challenge: &lt;strong&gt;bootstrapping event-driven services&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When you create a new service (like our Order Service), the upstream data already exists in other services. Users and products are already in the system, but your new Order Service has none of this data. How do you populate it?&lt;/p&gt;

&lt;p&gt;We'll explore strategies for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Initial data synchronization&lt;/strong&gt; when deploying new services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backfilling events&lt;/strong&gt; from existing data sources&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handling the gap&lt;/strong&gt; between service creation and going live&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ensuring consistency&lt;/strong&gt; during the bootstrap phase&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is essential for building truly independent services that don't rely on direct database access to other services' data.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The Inbox Pattern adds complexity, but for critical business operations requiring guaranteed processing, it's indispensable. Check out the &lt;a href="https://gitlab.com/vibetdd/examples/be/kotlin/3-api-user-product-order" rel="noopener noreferrer"&gt;complete example on GitLab&lt;/a&gt; to see it in action!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>eventdriven</category>
      <category>backend</category>
      <category>kotlin</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Event Handling: Keep It Fast and Simple</title>
      <dc:creator>Maksim Matlakhov</dc:creator>
      <pubDate>Tue, 30 Sep 2025 10:21:00 +0000</pubDate>
      <link>https://forem.com/maksim_matlakhov/event-handling-keep-it-fast-and-simple-5867</link>
      <guid>https://forem.com/maksim_matlakhov/event-handling-keep-it-fast-and-simple-5867</guid>
      <description>&lt;p&gt;When building event-driven systems, one of the biggest mistakes I see is treating all events the same way. A simple username update gets the same heavyweight processing as complex fraud detection logic. This leads to slow systems, unnecessary complexity, and hard-to-debug race conditions.&lt;/p&gt;

&lt;p&gt;The solution? Recognize that events fall into two distinct categories and handle them differently. Simple data updates need fast, direct processing. Complex business workflows need careful orchestration. By using the right approach for each type, you can build systems that are both performant and maintainable.&lt;/p&gt;

&lt;p&gt;In my &lt;a href="https://dev.to/maksim_matlakhov/messaging-broker-migration-why-lock-in-hurts-and-how-to-avoid-it-dj"&gt;previous post&lt;/a&gt;, I talked about treating messaging brokers as simple transport layers. Today, let's look at how to handle events the right way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Simple Rules
&lt;/h2&gt;

&lt;p&gt;Here are the basic rules I follow:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 1: Order and Uniqueness&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Every event needs an &lt;code&gt;eventId&lt;/code&gt; and &lt;code&gt;version&lt;/code&gt;. This helps you handle events in the right order and avoid processing the same event twice. Don't trust the messaging broker to do this for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 2: Process Fast&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Release events quickly. Don't do heavy work like complex calculations or calling external services right away. Store the event locally and do the heavy work later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 3: Simple vs Complex&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Some events are simple (like updating a user's name), others are complex (like fraud detection). Handle them differently.&lt;/p&gt;
&lt;h2&gt;
  
  
  Two Types of Events
&lt;/h2&gt;

&lt;p&gt;From my experience, events fall into two groups:&lt;/p&gt;
&lt;h3&gt;
  
  
  Type 1: Simple Updates
&lt;/h3&gt;

&lt;p&gt;Easy data updates with no business rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Update search indexes&lt;/li&gt;
&lt;li&gt;Sync read models&lt;/li&gt;
&lt;li&gt;Clear caches&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Type 2: Complex Work
&lt;/h3&gt;

&lt;p&gt;Multistep processes with business rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fraud detection&lt;/li&gt;
&lt;li&gt;Complex workflows&lt;/li&gt;
&lt;li&gt;External service calls&lt;/li&gt;
&lt;li&gt;Business rule checks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The difference matters because each type needs different handling.&lt;/p&gt;
&lt;h2&gt;
  
  
  How Type 1 Processing Works
&lt;/h2&gt;

&lt;p&gt;For simple updates, here's what happens when an event comes in:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Get Event&lt;/strong&gt;: Consumer receives event from messaging broker&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check Version&lt;/strong&gt;: Compare event version with stored data version&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skip Old Events&lt;/strong&gt;: If &lt;code&gt;event.version ≤ stored.version&lt;/code&gt;, ignore it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update Data&lt;/strong&gt;: Create new versioned data from event&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Save Changes&lt;/strong&gt;: Update database with optimistic locking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handle Conflicts&lt;/strong&gt;: Database throws an exception if someone else updated the record first&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry&lt;/strong&gt;: Message broker retries the event automatically&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fast Processing&lt;/strong&gt;: Direct database updates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safe Concurrency&lt;/strong&gt;: Optimistic locking prevents conflicts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version Control&lt;/strong&gt;: Prevents old events from overwriting new data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto Recovery&lt;/strong&gt;: Message broker handles retries&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Type 1 Implementation
&lt;/h2&gt;

&lt;p&gt;For simple updates, I use a generic helper that handles versioning and prevents conflicts.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Generic Helper
&lt;/h3&gt;

&lt;p&gt;Here's the main helper class that handles all simple updates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Generic orchestrator that works with any model type&lt;/span&gt;
&lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;VTViewOrchestrator&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;DATA&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;modelStorage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;VTModelStorageAdapter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;DATA&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// Create new model from event&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EVENT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventDtoBody&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventV1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EVENT&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
        &lt;span class="n"&gt;createModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;EVENT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;MODEL&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Don't create if already exists&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modelStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="c1"&gt;// Create new model with version 0&lt;/span&gt;
        &lt;span class="n"&gt;modelStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;createdAt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;updatedAt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Update specific field in existing model&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EVENT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventDtoBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;FIELD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventV1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EVENT&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
        &lt;span class="n"&gt;extractField&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;VersionedField&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FIELD&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Get current field&lt;/span&gt;
        &lt;span class="n"&gt;updateModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;VersionedField&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FIELD&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Set new field&lt;/span&gt;
        &lt;span class="n"&gt;buildFieldData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;EVENT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;FIELD&lt;/span&gt; &lt;span class="c1"&gt;// Build new data from event&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Get existing model, throws if not found (for cases when an update event comes first)&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;storedModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;modelStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRequired&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;currentField&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;VersionedField&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FIELD&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;storedModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;// Skip if this event is older than stored data&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentField&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="c1"&gt;// Create new field with event data and version&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;newField&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;VersionedField&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FIELD&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;VersionedField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildFieldData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;// Update model with new field&lt;/span&gt;
        &lt;span class="n"&gt;modelStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;storedModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;updatedAt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;updateModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;storedModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newField&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Versioned Fields for Concurrency
&lt;/h3&gt;

&lt;p&gt;The key is using versioned fields so different parts can update independently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Each field has its own version&lt;/span&gt;
&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;UserView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;personalData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;VersionedField&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PersonalData&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Version for name, email, etc.&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;VersionedField&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt;       &lt;span class="c1"&gt;// Version for status changes&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Wrapper that tracks version for any data type&lt;/span&gt;
&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;VersionedField&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Event Consumer
&lt;/h3&gt;

&lt;p&gt;Here's how you use the helper to handle events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@EventConsumer&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserEventsConsumerV1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;orchestrator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UserViewOrchestrator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onCreated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventV1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserCreatedV1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;orchestrator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Build initial user view from event&lt;/span&gt;
            &lt;span class="nc"&gt;UserView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;personalData&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;VersionedField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PersonalData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;personalData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;VersionedField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;
                    &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onPersonalDataUpdated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventV1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PersonalDataUpdatedV1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;orchestrator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;extractField&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;personalData&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;        &lt;span class="c1"&gt;// Get current personal data field&lt;/span&gt;
            &lt;span class="n"&gt;updateModel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;             &lt;span class="c1"&gt;// How to update the model&lt;/span&gt;
                &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;personalData&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;buildFieldData&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                          &lt;span class="c1"&gt;// Build new data from event&lt;/span&gt;
                &lt;span class="nc"&gt;PersonalData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onStatusUpdated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventV1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserStatusUpdatedV1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;orchestrator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;extractField&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;             
            &lt;span class="n"&gt;updateModel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;            
                &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;buildFieldData&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                         
                &lt;span class="nc"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Service Setup
&lt;/h3&gt;

&lt;p&gt;The concrete orchestrator is just a Spring component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserViewOrchestrator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UserViewStorage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Handles database operations&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;VTViewOrchestrator&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;UserViewDoc&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the mongo document with common version field that prevents concurrent update:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"view-users"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;UserViewDoc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;Version&lt;/span&gt; &lt;span class="c1"&gt;// Does the magic&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UserView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why This Works So Well
&lt;/h2&gt;

&lt;p&gt;The generic helper pattern gives you several benefits:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Same Pattern Everywhere&lt;/strong&gt;: All services use the same approach for handling events&lt;br&gt;
&lt;strong&gt;Reusable Code&lt;/strong&gt;: One helper works for all your models&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Type Safety&lt;/strong&gt;: Kotlin ensures you can't make mistakes with field types&lt;br&gt;
&lt;strong&gt;Easy to Maintain&lt;/strong&gt;: Fix bugs in one place, all services benefit&lt;br&gt;
&lt;strong&gt;Fast Performance&lt;/strong&gt;: Direct database updates without extra overhead&lt;/p&gt;

&lt;p&gt;The versioned field approach solves a common problem: event ordering. Instead of trusting the message broker to deliver events in order (which I don't recommend), each field tracks its own version.&lt;/p&gt;

&lt;p&gt;This means a "name updated" event with version 5 won't overwrite data from a "status updated" event with version 7. Each field can be updated independently without conflicts.&lt;/p&gt;

&lt;p&gt;At the same time the common &lt;code&gt;version&lt;/code&gt; field prevents a model inconsistency when different field updates come simultaneously.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next: Complex Events
&lt;/h2&gt;

&lt;p&gt;Type 1 events handle most of your event processing needs efficiently. But what about complex business logic that needs multiple steps, external service calls, or special error handling?&lt;/p&gt;

&lt;p&gt;That's where Type 2 events come in. In my next post, I'll show you how to handle complex business workflows using internal event storage and Spring's event system, while keeping your architecture clean.&lt;/p&gt;

&lt;p&gt;The main idea is simple: recognize that not all events need the same processing approach. By classifying events and using the right pattern for each type, you can build systems that are both fast and easy to maintain.&lt;/p&gt;

</description>
      <category>eventdriven</category>
      <category>architecture</category>
      <category>kotlin</category>
      <category>backend</category>
    </item>
    <item>
      <title>Messaging Broker Migration: Why Lock-in Hurts and How to Avoid It</title>
      <dc:creator>Maksim Matlakhov</dc:creator>
      <pubDate>Mon, 29 Sep 2025 11:18:00 +0000</pubDate>
      <link>https://forem.com/maksim_matlakhov/messaging-broker-migration-why-lock-in-hurts-and-how-to-avoid-it-dj</link>
      <guid>https://forem.com/maksim_matlakhov/messaging-broker-migration-why-lock-in-hurts-and-how-to-avoid-it-dj</guid>
      <description>&lt;p&gt;Have you ever struggled with messaging broker migration? I've heard this opinion countless times: "Why do we need that? We're on Amazon and use SNS/SQS - it works perfectly." While that might be true today, what happens when things change?&lt;/p&gt;

&lt;p&gt;The reality is that business needs change, and what works today might not work tomorrow. Let me share why messaging broker lock-in is a problem and how to build systems that can adapt easily.&lt;/p&gt;

&lt;h2&gt;
  
  
  When You Need to Migrate
&lt;/h2&gt;

&lt;p&gt;Even the most satisfied AWS (or another provider) users might face situations where migration becomes needed:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Business Opportunities&lt;/strong&gt;: You're a startup built on AWS and Google offers you $200K in credits through their startup program. That's real money that could help your business grow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Market Innovation&lt;/strong&gt;: A new messaging broker appears that's much more efficient and cheaper than your current solution. Do you want to be stuck with old technology?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strategic Changes&lt;/strong&gt;: You decide to build your own messaging provider or move to a completely different setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compliance Requirements&lt;/strong&gt;: New regulations require data to stay in specific regions that your current provider doesn't support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance Problems&lt;/strong&gt;: Your current broker can't handle growing message volumes (or can but igh cost) or speed requirements, but alternatives can.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost Problems&lt;/strong&gt;: What happens when your provider decides to raise prices a lot? You need options.&lt;/p&gt;

&lt;p&gt;I've seen some of these situations, and those with tightly connected messaging systems found themselves in painful situations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Migration Nightmare: Common Anti-Patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Anti-Pattern 1: Broker as Event Storage
&lt;/h3&gt;

&lt;p&gt;The worst case from my perspective is when teams use the broker as their primary event storage. You're essentially married to that provider forever, and they can do whatever they want - including increasing costs dramatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Anti-Pattern 2: Dual-Send Without Atomicity
&lt;/h3&gt;

&lt;p&gt;Some teams think they can solve migration by adding a new line to send messages to a new provider alongside the existing one. Apart from the original timing problems you had with two operations (storing the model and sending the message), you now have a third operation. What happens when one of them fails?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;orderRepository&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;OrderRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;kafkaPublisher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;KafkaPublisher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;rabbitPublisher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;RabbitPublisher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BrokerProducer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
        &lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;kafkaPublisher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;rabbitPublisher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// One more point of failure to prevent an order saving?&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Anti-Pattern 3: Feature-Dependent Architecture
&lt;/h3&gt;

&lt;p&gt;Many systems become deeply dependent on broker-specific features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FIFO queue guarantees for ordering&lt;/li&gt;
&lt;li&gt;Exactly-once delivery promises&lt;/li&gt;
&lt;li&gt;Dead letter queue mechanisms&lt;/li&gt;
&lt;li&gt;Broker-specific retry policies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When migration time comes, copying these features across different brokers becomes a complex engineering challenge.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Path to Broker Independence
&lt;/h2&gt;

&lt;p&gt;To make migration easier, we need to treat message brokers as a &lt;strong&gt;transport layer only&lt;/strong&gt; and avoid using broker-specific features. Here's how:&lt;/p&gt;

&lt;h3&gt;
  
  
  Design Principles
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Don't Rely on Event Ordering&lt;/strong&gt;: Instead of depending on FIFO queues, make your consumers handle ordering. Each event should have a unique ID and creation timestamp to help manage sequencing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Handle Duplicates Well&lt;/strong&gt;: Don't rely on exactly-once delivery guarantees. Design your consumers to handle the same message multiple times without problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Avoid Dead Letter Queues&lt;/strong&gt;: If you store events in your database, you don't need broker-managed dead letter queues. I remember how painful it was to manage and restore events from dead letter queues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Control Your Own Destiny&lt;/strong&gt;: Store events in your database where you control delivery status, error handling, and retries. If attempts reach a limit due to network issues, it's easy to resend and retry because you control your database.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Migration Strategy: Multi-Broker Publishing
&lt;/h2&gt;

&lt;p&gt;Here's how I designed the system to handle broker migration seamlessly:&lt;/p&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;processedBrokers&lt;/code&gt; Field
&lt;/h3&gt;

&lt;p&gt;Apart from storing multiple event DTO versions (which I described in &lt;a href="https://dev.to/maksim_matlakhov/seamless-events-version-management-4o4e"&gt;my previous post&lt;/a&gt;), I store a &lt;code&gt;processedBrokers&lt;/code&gt; field with each event. During event execution, the system iterates through all implementations of &lt;code&gt;BrokerProducer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here's how an event looks in MongoDB with the &lt;code&gt;processedBrokers&lt;/code&gt; tracking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1cc64bb3-17f0-3619-a68d-a2564bf1644d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"topic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user.model.created.v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"notification"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SENT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"attempts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"failedReasons"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"processedBrokers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"kafka"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"spring"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the &lt;code&gt;processedBrokers&lt;/code&gt; array contains both "kafka" and "spring", indicating this event was successfully sent to both brokers. The &lt;code&gt;attempts&lt;/code&gt; counter and &lt;code&gt;failedReasons&lt;/code&gt; array help track delivery issues and retry logic.&lt;/p&gt;

&lt;p&gt;This approach allows us to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Map multiple brokers to the same event&lt;/li&gt;
&lt;li&gt;Send the same message to all configured brokers&lt;/li&gt;
&lt;li&gt;Avoid resending to already-processed brokers during retries&lt;/li&gt;
&lt;li&gt;Handle both external brokers (Kafka, SQS) and internal events (Spring pub/sub)&lt;/li&gt;
&lt;li&gt;Track delivery status per broker independently&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Spring Pub/Sub Example
&lt;/h3&gt;

&lt;p&gt;Let me walk you through a concrete example using the code snippets you provided. We'll look at how the system handles internal Spring pub/sub messaging, which demonstrates the same principles used for external broker migration.&lt;/p&gt;

&lt;h4&gt;
  
  
  Producer Implementation
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="nd"&gt;@ConditionalOnProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"vt.events.producer.type.spring.enabled"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;havingValue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;matchIfMissing&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SpringBrokerProducer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;eventPublisher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ApplicationEventPublisher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BrokerProducer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;broker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"spring"&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BrokerMessageV1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;eventPublisher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publishEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PayloadApplicationEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;SpringBrokerProducer&lt;/code&gt; implements the same &lt;code&gt;BrokerProducer&lt;/code&gt; interface as our Kafka, SQS, or RabbitMQ producers. The system automatically discovers all implementations and sends events to each configured broker.&lt;/p&gt;

&lt;h4&gt;
  
  
  Consumer Implementation
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="nd"&gt;@ConditionalOnProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"vt.events.consumer.type.spring.enabled"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;havingValue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;matchIfMissing&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SpringEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;topicSubscriber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SpringTopicSubscriber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;log&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KotlinLogging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="nd"&gt;@EventListener&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PayloadApplicationEvent&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;BrokerMessageV1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;topic&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;consumers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;MutableList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Consumer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;topicSubscriber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;topicConsumers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;consumers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"No consumers found for topic '$topic'"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;consumers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;consumer&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"Processing event for topic '$topic' with consumer '${consumer.type.simpleName}'"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="n"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"Error in consumer handler for topic '$topic': ${event.payload}"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The beauty of this design is that the same consumer code works regardless of the underlying broker. The framework abstracts away the transport layer completely.&lt;/p&gt;

&lt;h4&gt;
  
  
  Topic Subscription
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="nd"&gt;@ConditionalOnProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"vt.events.consumer.type.spring.enabled"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;havingValue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;matchIfMissing&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SpringTopicSubscriber&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TopicSubscriber&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;log&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KotlinLogging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;topicConsumers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mutableMapOf&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;MutableList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Consumer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;()&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consumers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;MutableList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Consumer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;topicConsumers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;consumers&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Registered consumers: ${consumers.map { it.type.simpleName }} for topic: $topic"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configuration Management
&lt;/h3&gt;

&lt;p&gt;Notice how both producer and consumer use &lt;code&gt;@ConditionalOnProperty&lt;/code&gt; annotations. This allows us to control which brokers are active through configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Enable Spring internal messaging
vt.events.producer.type.spring.enabled=true
vt.events.consumer.type.spring.enabled=true

# Enable Kafka
vt.events.producer.type.kafka.enabled=true
vt.events.consumer.type.kafka.enabled=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Maven Module Organization
&lt;/h3&gt;

&lt;p&gt;The modular structure from your &lt;code&gt;pom.xml&lt;/code&gt; files shows another layer of flexibility:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Spring messaging module --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;dev.vibetdd.kotlin&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;vt-messaging-consumer-spring&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Kafka messaging module --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;dev.vibetdd.kotlin&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;vt-messaging-consumer-kafka&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can manage broker support by including/excluding Maven modules, similar to the API client approach I described in &lt;a href="https://dev.to/posts/2025/09/from-monolith-to-standalone-service"&gt;the monolith split post&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Migration Process in Action
&lt;/h2&gt;

&lt;p&gt;Here's how a typical migration would work with this system:&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1: Producer Preparation
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add New Broker Module&lt;/strong&gt;: Include the new broker's Maven dependency&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy Producer&lt;/strong&gt;: The system automatically starts sending to both brokers&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Phase 2: Consumer Migration
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Update Client Dependencies&lt;/strong&gt;: Consumer services update their event library dependency&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gradual Migration&lt;/strong&gt;: Each consumer team switches to the new broker when ready&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Coordination Required&lt;/strong&gt;: Teams work independently during the transition period&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Phase 3: Cleanup
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Monitor Delivery&lt;/strong&gt;: Ensure all consumers successfully receive events from the new broker&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove Old Broker&lt;/strong&gt;: Disable the old broker configuration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean Dependencies&lt;/strong&gt;: Remove old broker Maven modules&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Benefits of This Approach
&lt;/h2&gt;

&lt;p&gt;This approach is particularly valuable for:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Startup Growth&lt;/strong&gt;: Early-stage companies can start with simple solutions using internal messaging and evolve to enterprise-grade messaging without architectural rewrites.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monolith Migration&lt;/strong&gt;: When extracting services from a monolith, you can start replacing internal messaging to external brokers as services become independent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloud Migration&lt;/strong&gt;: Moving between cloud providers becomes much less risky when your messaging layer can adapt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compliance Changes&lt;/strong&gt;: When regulations require data to stay in specific regions, you can quickly change your messaging setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Messaging broker lock-in is a real problem that teams often don't recognize until it's too late. By designing your event-driven architecture with broker independence from the start, you maintain the flexibility to adapt as your business needs evolve.&lt;/p&gt;

&lt;p&gt;The key insights are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Treat brokers as transport layers, not feature providers&lt;/li&gt;
&lt;li&gt;Store events in your database for complete control&lt;/li&gt;
&lt;li&gt;Use abstraction layers that hide broker-specific implementation details&lt;/li&gt;
&lt;li&gt;Design for multi-broker publishing from day one&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Remember: the best time to prepare for migration is when you don't need it yet. By the time migration becomes urgent, it's often too late to implement these patterns without significant pain.&lt;/p&gt;

&lt;p&gt;Your future self (and your team) will thank you for building systems that can adapt rather than systems that lock you in.&lt;/p&gt;

</description>
      <category>eventdriven</category>
      <category>architecture</category>
      <category>kotlin</category>
      <category>backend</category>
    </item>
    <item>
      <title>Seamless Events Version Management</title>
      <dc:creator>Maksim Matlakhov</dc:creator>
      <pubDate>Sun, 28 Sep 2025 10:17:18 +0000</pubDate>
      <link>https://forem.com/maksim_matlakhov/seamless-events-version-management-4o4e</link>
      <guid>https://forem.com/maksim_matlakhov/seamless-events-version-management-4o4e</guid>
      <description>&lt;p&gt;One of the biggest pain points in event-driven systems comes when you need to make breaking changes to your event structure. I've seen teams struggle with this countless times: a simple field rename or restructuring forces a complex, coordinated deployment across multiple services, sometimes requiring temporary downtime or complicated migration strategies.&lt;/p&gt;

&lt;p&gt;In some systems I've worked with, when you change an event structure, you face a chicken-and-egg problem. Consumers need to understand the new format before producers can start sending it, but producers can't stop sending the old format until all consumers are updated. This creates a deployment nightmare, especially in distributed systems where different teams own different services.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pain of Breaking Changes
&lt;/h2&gt;

&lt;p&gt;Consider a scenario: your user registration event initially had a simple &lt;code&gt;name&lt;/code&gt; field, but business requirements now demand separate &lt;code&gt;firstName&lt;/code&gt; and &lt;code&gt;lastName&lt;/code&gt; fields for better personalization. Additionally, for privacy reasons, you need to stop storing the &lt;code&gt;email&lt;/code&gt; field (we have a better place for that: Firebase, Auth0 etc).&lt;/p&gt;

&lt;p&gt;From my experience, this scenario typically requires:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Coordinated deployment&lt;/strong&gt;: All consumers must be updated before the producer changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complex versioning logic&lt;/strong&gt;: Producers need conditional logic to send different versions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temporary compatibility layers&lt;/strong&gt;: Extra code to handle both old and new formats&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Risk of service disruption&lt;/strong&gt;: Any mistake in the coordination can break the system&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Opening the Door to Seamless Evolution
&lt;/h2&gt;

&lt;p&gt;My framework takes a different approach - it keeps the door open for evolution from day one. Instead of treating event versions as mutually exclusive, the system assumes that a single model event can be mapped and published to multiple DTO event versions simultaneously.&lt;/p&gt;

&lt;p&gt;Here's the key insight: each event version has the same core metadata (eventId, modelId, version, timestamp) but different topic names that include the DTO version (&lt;code&gt;.v1&lt;/code&gt;, &lt;code&gt;.v2&lt;/code&gt;, etc.). The framework automatically detects all available mappers and publishes to every configured version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step-by-Step Migration Process
&lt;/h2&gt;

&lt;p&gt;Let's walk through our example transformation using &lt;a href="https://gitlab.com/vibetdd/examples/be/kotlin/2-users-payouts/users/-/commit/b718fc939768e43e21802d3054cab029a734e6b6" rel="noopener noreferrer"&gt;the code&lt;/a&gt; provided:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Create the New Event Structure
&lt;/h3&gt;

&lt;p&gt;First, we define the new data structure with separated name fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;PersonalDataV2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;UserCreatedV2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;personalData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PersonalDataV2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;StatusV1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventDtoBody&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Define the New Topic Version
&lt;/h3&gt;

&lt;p&gt;Create a new topic enum that follows the same pattern but with version 2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UsersEventTopicV2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;eventClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;KClass&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="nc"&gt;EventDtoBody&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventAction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Increment the version&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventTopic&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;CREATED&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserCreatedV2&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;EventAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;created&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates the topic &lt;code&gt;user.model.created.v2&lt;/code&gt; alongside the existing &lt;code&gt;user.model.created.v1&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Implement the New Mapper
&lt;/h3&gt;

&lt;p&gt;The framework automatically discovers mappers through Spring's component scanning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserCreatedMapperV2&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventMapper&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserCreated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;UserCreatedV2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="n"&gt;modelClass&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UserCreated&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;topic&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UsersEventTopicV2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CREATED&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nc"&gt;UserCreated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mapToDto&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;UserCreatedV2&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;nameParts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;personalData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;UserCreatedV2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;personalData&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PersonalDataV2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;firstName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nameParts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;firstOrNull&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;orEmpty&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;lastName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nameParts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lastOrNull&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;orEmpty&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StatusV1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nc"&gt;UserCreatedV2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mapToModel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UserCreated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;personalData&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PersonalData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"${personalData.firstName} ${personalData.lastName}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UserStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;valueOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how the mapping handles the transformation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Splits the existing &lt;code&gt;name&lt;/code&gt; field into &lt;code&gt;firstName&lt;/code&gt; and &lt;code&gt;lastName&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Removes the email field from the V2 event&lt;/li&gt;
&lt;li&gt;Provides reverse mapping for transforming DTO to the event model&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Update Consumers at Your Own Pace
&lt;/h3&gt;

&lt;p&gt;On &lt;a href="https://gitlab.com/vibetdd/examples/be/kotlin/2-users-payouts/payouts/-/commit/ac1f8f0738c4d426b11ad3603b457eff3900157c" rel="noopener noreferrer"&gt;the consumer side&lt;/a&gt;, teams can migrate independently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@EventConsumer&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserEventsConsumerV2&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onCreated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventV1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserCreatedV2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="c1"&gt;// Handle me&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The consumer simply needs to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a new consumer class&lt;/li&gt;
&lt;li&gt;Subscribe to the V2 topic&lt;/li&gt;
&lt;li&gt;Handle the new event structure&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. The Magic Happens Automatically
&lt;/h3&gt;

&lt;p&gt;Once you deploy the producer service with both mappers, the system automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detects both V1 and V2 mappers for the &lt;code&gt;UserCreated&lt;/code&gt; model&lt;/li&gt;
&lt;li&gt;Maps each event to both DTO versions&lt;/li&gt;
&lt;li&gt;Stores both versions in the event store&lt;/li&gt;
&lt;li&gt;Publishes to both topics (&lt;code&gt;user.model.created.v1&lt;/code&gt; and &lt;code&gt;user.model.created.v2&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Event Storage: Both Versions Coexist
&lt;/h2&gt;

&lt;p&gt;Here's how the events look in MongoDB - notice they share the same metadata but have different topic names and body structures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;V&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Event&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1cc64bb3-17f0-3619-a68d-a2564bf1644d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"topic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user.model.created.v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"personalData"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"John Smith"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"example@vibetdd.dev"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ACTIVE"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"eventId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"befadebf-66ff-3c50-a068-8468856189d8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"modelId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fc619c9e-23aa-3210-bad2-d69bc2e35e13"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-09-25T15:13:15.772+0000"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"notification"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SENT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"processedBrokers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"kafka"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;V&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Event&lt;/span&gt;&lt;span class="w"&gt;  
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ea196b5c-0da0-30fd-9c11-92d9f3dc5b37"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"topic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user.model.created.v2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"personalData"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"firstName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"John"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"lastName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Smith"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ACTIVE"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"eventId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"befadebf-66ff-3c50-a068-8468856189d8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"modelId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fc619c9e-23aa-3210-bad2-d69bc2e35e13"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
      &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-09-25T15:13:15.772+0000"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"notification"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SENT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"processedBrokers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"kafka"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cleanup: Removing the Old Version
&lt;/h2&gt;

&lt;p&gt;Once you've confirmed all consumers have migrated to V2, cleanup is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Remove the &lt;code&gt;UserCreatedV1&lt;/code&gt; class&lt;/li&gt;
&lt;li&gt;Remove the &lt;code&gt;UserCreatedMapperV1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Remove the &lt;code&gt;UsersEventTopicV1.CREATED&lt;/code&gt; enum entry&lt;/li&gt;
&lt;li&gt;Remove the &lt;code&gt;PersonalDataV1&lt;/code&gt; class if no longer used&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The system will automatically detect that only the V2 mapper exists and publish only the V2 events going forward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benefits of This Approach
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Zero-Downtime Migration&lt;/strong&gt;: Consumers and producers can be updated independently without any service interruption.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gradual Rollout&lt;/strong&gt;: You can migrate consumers one by one, testing each step thoroughly before proceeding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rollback Safety&lt;/strong&gt;: If issues arise, you can quickly revert consumers to the V1 topic while investigating.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audit Trail&lt;/strong&gt;: Both event versions are preserved in storage, providing a complete history of what was sent to different consumers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No Coordination Required&lt;/strong&gt;: Teams can work independently without complex deployment choreography.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Framework's Philosophy
&lt;/h2&gt;

&lt;p&gt;This design reflects a core philosophy: &lt;strong&gt;embrace change rather than fight it&lt;/strong&gt;. Instead of treating event evolution as a problem to be solved with complex tooling, the framework makes evolution a first-class citizen. By assuming events will evolve and preparing for it from the start, we eliminate the pain that traditionally comes with event schema changes.&lt;/p&gt;

&lt;p&gt;The automatic discovery and mapping approach also makes the system AI-friendly - when you need to create a new event version, you simply follow the established patterns, and the framework handles the rest. No manual registration, no complex configuration files, just clean, predictable code that works.&lt;/p&gt;

</description>
      <category>eventdriven</category>
      <category>architecture</category>
      <category>kotlin</category>
      <category>backend</category>
    </item>
    <item>
      <title>Introducing a Hybrid Event Sourcing Framework for Modern Applications</title>
      <dc:creator>Maksim Matlakhov</dc:creator>
      <pubDate>Fri, 26 Sep 2025 10:40:24 +0000</pubDate>
      <link>https://forem.com/maksim_matlakhov/introducing-a-hybrid-event-sourcing-framework-for-modern-applications-2jpm</link>
      <guid>https://forem.com/maksim_matlakhov/introducing-a-hybrid-event-sourcing-framework-for-modern-applications-2jpm</guid>
      <description>&lt;p&gt;Event sourcing has gained significant traction in recent years, promising complete audit trails, temporal queries, and robust system architecture. However, pure event sourcing often introduces complexity that can overwhelm development teams. Today, I want to introduce a hybrid event sourcing approach I've integrated into my framework that captures the benefits of event sourcing while maintaining operational simplicity.&lt;/p&gt;

&lt;p&gt;The framework is designed to be compatible with the Event Modeling methodology, strictly following the command/event/read model pattern with clear boundaries between these building blocks. However, it's not limited to Event Modeling - we use a similar approach in my current company, and it works very well in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge with Pure Event Sourcing
&lt;/h2&gt;

&lt;p&gt;Based on my experience trying to implement various event-driven systems, pure event sourcing comes with real-world challenges that I've encountered:&lt;/p&gt;

&lt;h3&gt;
  
  
  Race Conditions and Conflict Resolution
&lt;/h3&gt;

&lt;p&gt;One of challenges I've faced is managing race conditions in update operations. Consider this scenario: two users update a product status simultaneously. In pure event sourcing, this typically requires:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Optimistic locking mechanisms&lt;/strong&gt; - Using unique constraints, something like &lt;code&gt;{aggregate_id, sequence_id}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conflict resolution logic&lt;/strong&gt; - Determining which update wins and handling the "loser"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry mechanisms&lt;/strong&gt; - Failed operations must retry based on the latest system state&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User notification&lt;/strong&gt; - Informing users about conflicts and requiring manual resolution&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;While these solutions work, they add significant complexity to the system. Event sourcing advocates often suggest that optimistic locking makes this "much simpler," but in my experience, implementing robust conflict resolution that handles all edge cases gracefully requires substantial engineering effort or relying on a magic using an event sourcing framework.&lt;/p&gt;

&lt;h3&gt;
  
  
  Read Model Performance and Complexity
&lt;/h3&gt;

&lt;p&gt;Building read models from events presents several practical challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Event replay overhead&lt;/strong&gt; - Even with tons of events, projection rebuilding can be time-consuming&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple projection maintenance&lt;/strong&gt; - Each new query pattern requires a new projection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event schema evolution&lt;/strong&gt; - Changing event structures requires migration of all dependent projections&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eventual Consistency&lt;/strong&gt; - Read models are eventually consistent by nature, but can create user experience issues when immediate read-after-write consistency is expected&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debugging complexity&lt;/strong&gt; - Troubleshooting issues requires understanding the entire event history&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While the flexibility of having multiple projections (&lt;code&gt;user_history&lt;/code&gt;, &lt;code&gt;user_orders&lt;/code&gt;, &lt;code&gt;disabled_users&lt;/code&gt;) is powerful, it comes with operational overhead that many teams underestimate.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Root Issue
&lt;/h3&gt;

&lt;p&gt;I want to clarify that these aren't inherent flaws in event sourcing - they're general distributed system challenges that pure event sourcing doesn't solve automatically. The theoretical benefits are compelling, but the practical implementation complexity often outweighs the advantages for many use cases.&lt;/p&gt;

&lt;p&gt;My approach addresses these challenges by combining events with current state storage, making conflict resolution simpler and basic read operations more straightforward while preserving the audit trail and integration benefits that make event sourcing attractive.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Hybrid Approach
&lt;/h2&gt;

&lt;p&gt;Instead of pure event sourcing, I've developed a hybrid system that combines the audit trail benefits of events with the simplicity of current state storage. Here's how it works:&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Principles
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Generate events for every action&lt;/strong&gt;: User created, status changed, payout requested, order canceled&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Events belong to specific models&lt;/strong&gt;: Similar to aggregates in event sourcing terms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transactional consistency&lt;/strong&gt;: Events and model updates happen in a single transaction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Independent model parts&lt;/strong&gt;: Different aspects of a model can be updated independently with their own versioning&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Model Structure
&lt;/h3&gt;

&lt;p&gt;My models can be simple or have multiple parts that update independently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simple model - single entity&lt;/span&gt;
&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Comment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Complex model - multiple independent parts&lt;/span&gt;
&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ProductDescription&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// Can be updated independently&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                   &lt;span class="c1"&gt;// Can be updated independently  &lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ProductPrice&lt;/span&gt;               &lt;span class="c1"&gt;// Can be updated independently&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This design eliminates false conflicts. An admin updating product description won't conflict with another admin updating pricing, as they operate on different parts with separate versioning.&lt;/p&gt;

&lt;p&gt;Events are controlled through a composite identifier consisting of &lt;code&gt;modelId + action + version&lt;/code&gt;. Here are examples of how events are stored in MongoDB:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User Created Event:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6b846115-e41c-35db-ab27-12f8b3e99591"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"topic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user.model.created.v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"personalData"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"John Smith"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"example@vibetdd.dev"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ACTIVE"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"eventId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"454460ab-cfb5-3b7d-a9c6-e39f13f2dd23"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"modelId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a1a87c49-670e-3844-a2df-368c77f207a9"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-09-25T09:51:07.712Z"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Personal Data Updated Event:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cef0220d-cd94-3aa3-af33-b68a7f3d0db9"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"topic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user.personal-data.updated.v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"previous"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"John Smith"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"example@vibetdd.dev"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"current"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Will Smith"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
        &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"example@vibetdd.dev"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"eventId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3ae2cd7d-cba0-37cd-a6d4-3f0145571d4c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"modelId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a1a87c49-670e-3844-a2df-368c77f207a9"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-09-25T09:52:03.046Z"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Event Storage Architecture
&lt;/h2&gt;

&lt;p&gt;The event storage follows a clean separation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Database per domain/service&lt;/strong&gt;: Each service maintains its own events&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Common event collection&lt;/strong&gt;: All event types stored in a single table/collection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Current state storage&lt;/strong&gt;: Separate storage for model current state&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message broker integration&lt;/strong&gt;: Events processed asynchronously for consumers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple event versions&lt;/strong&gt;: Enables seamless migration between event DTO versions (this will be covered in a separate post)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A background processor constantly polls for pending events, determines which message brokers to send to, handles errors, and manages retries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: Users and Payouts Services
&lt;/h2&gt;

&lt;p&gt;Let's see how this works in practice with two services: &lt;a href="https://gitlab.com/vibetdd/examples/be/kotlin/2-users-payouts/users" rel="noopener noreferrer"&gt;users&lt;/a&gt; and &lt;a href="https://gitlab.com/vibetdd/examples/be/kotlin/2-users-payouts/payouts" rel="noopener noreferrer"&gt;payouts&lt;/a&gt;. The users service handles CRUD operations and sends events for every operation, while the payouts service consumes user events to make business decisions.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://gitlab.com/vibetdd/examples/be/kotlin/2-users-payouts/users" rel="noopener noreferrer"&gt;Users Service&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;The users service demonstrates the complete event creation flow. For updates, there are two cases: updating personal data with version control and status updates without version control.&lt;/p&gt;

&lt;p&gt;Personal data updates require version control - the client must request the current version before updating and send it back. If versions don't match, a conflict exception is thrown. Status updates, however, don't require version control as they're considered independent operations.&lt;/p&gt;

&lt;h4&gt;
  
  
  Event Creation
&lt;/h4&gt;

&lt;p&gt;Every operation starts with creating an event. Here's how I create a new user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateUserUseCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;idProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IdProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;validator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CommandValidator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;eventOrchestrator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventOrchestrator&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SaveCommandUseCase&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CreateUserCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CreateUserCommand&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Step 1: Validate the incoming command&lt;/span&gt;
        &lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;// Step 2: Create the event command with all necessary data&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;event&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CreateEventCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;modelId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// Generate deterministic ID from email&lt;/span&gt;
            &lt;span class="n"&gt;actor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Track who performed the action&lt;/span&gt;
            &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UserCreated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="c1"&gt;// The actual event body with business data&lt;/span&gt;
                &lt;span class="n"&gt;personalData&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PersonalData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ACTIVE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Default status for new users&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;// Step 3: Use event orchestrator to persist event and create model in single transaction&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;eventOrchestrator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// This lambda defines how to build the model from the event&lt;/span&gt;
            &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;personalData&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;personalData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The business logic only needs to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build the proper event&lt;/li&gt;
&lt;li&gt;Create mapping to the related model&lt;/li&gt;
&lt;li&gt;Let the system handle storage, processing, and notification&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Event Topic Structure
&lt;/h4&gt;

&lt;p&gt;Every event is mapped to a topic following the format: &lt;code&gt;model.subject.action.dto-version&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UsersEventTopicV1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;eventClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;KClass&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="nc"&gt;EventDtoBody&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventAction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventTopic&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;CREATED&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserCreatedV1&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;EventAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;created&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="nc"&gt;DELETED&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserDeletedV1&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;EventAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deleted&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="nc"&gt;PERSONAL_DATA_UPDATED&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PersonalDataUpdatedV1&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;EventAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserModelSubject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PERSONAL_DATA&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="nc"&gt;STATUS_UPDATED&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserStatusUpdatedV1&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;EventAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserModelSubject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;STATUS&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;UserModelSubject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;PERSONAL_DATA&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"personal-data"&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;STATUS&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"status"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates topics like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;user.model.created.v1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;user.personal-data.updated.v1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;user.status.updated.v1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Event Mapping and Versioning
&lt;/h4&gt;

&lt;p&gt;Events are mapped between internal models and external DTOs using dedicated mappers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserCreatedMapper&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventMapper&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserCreated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;UserCreatedV1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="n"&gt;modelClass&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UserCreated&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;topic&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UsersEventTopicV1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CREATED&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nc"&gt;UserCreated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mapToDto&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UserCreatedV1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;personalData&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PersonalDataV1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;personalData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;personalData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StatusV1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nc"&gt;UserCreatedV1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mapToModel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UserCreated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;personalData&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PersonalData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;personalData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;personalData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UserStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;valueOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This mapping layer enables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Version migration&lt;/strong&gt;: Multiple versions of the same event can coexist&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backward compatibility&lt;/strong&gt;: Consumers can upgrade at their own pace&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean boundaries&lt;/strong&gt;: Internal models remain separate from external contracts&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Handling Updates
&lt;/h4&gt;

&lt;p&gt;The framework supports two versioning approaches:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With Version Control&lt;/strong&gt; - For critical data that requires conflict detection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UpdateUserPersonalDataUseCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;validator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CommandValidator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;modelStorage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UserStoragePort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;eventOrchestrator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventOrchestrator&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SaveCommandUseCase&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UpdateUserPersonalDataCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UpdateUserPersonalDataCommand&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Step 1: Get the current stored model&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;storedModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;modelStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRequired&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PersonalData&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildUpdated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;storedModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;storedModel&lt;/span&gt;

        &lt;span class="c1"&gt;// Step 2: Validate the command&lt;/span&gt;
        &lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;// Step 3: Create event with expected version for conflict detection&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;event&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CreateEventCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;modelId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;expectedVersion&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;versionRequired&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Update personal data"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// Version control&lt;/span&gt;
            &lt;span class="n"&gt;actor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PersonalDataUpdated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;previous&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;storedModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;personalData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;// Step 4: Update model with event orchestrator&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;eventOrchestrator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;personalData&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// If there are no changed then return the same version&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;buildUpdated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;storedModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UpdateUserPersonalDataCommand&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;PersonalData&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;updated&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;storedModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;personalData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;storedModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;personalData&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Without Version Control&lt;/strong&gt; - For independent updates where conflicts are acceptable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UpdateUserStatusUseCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;modelStorage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UserStoragePort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;eventOrchestrator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventOrchestrator&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SaveCommandUseCase&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UpdateUserStatusCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UpdateUserStatusCommand&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Step 1: Get the current stored model&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;storedModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;modelStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRequired&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserStatus&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildUpdated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;storedModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;storedModel&lt;/span&gt;

        &lt;span class="c1"&gt;// Step 2: Create event without expected version (no version control)&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;event&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CreateEventCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;modelId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;actor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// No expectedVersion parameter&lt;/span&gt;
            &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UserStatusUpdated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;previous&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;storedModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;// Step 3: Update model with event orchestrator&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;eventOrchestrator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;buildUpdated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;storedModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UpdateUserStatusCommand&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserStatus&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;storedModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nc"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;a href="https://gitlab.com/vibetdd/examples/be/kotlin/2-users-payouts/payouts" rel="noopener noreferrer"&gt;Payouts Service&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;The payouts service demonstrates event consumption. It includes the users service events client dependency and creates a consumer to handle relevant user events.&lt;/p&gt;

&lt;h4&gt;
  
  
  Add Dependency
&lt;/h4&gt;

&lt;p&gt;Include the events client dependency in your service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;vt.demo.service&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;users-client-events&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;${client.users.version}&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Create Consumer
&lt;/h4&gt;

&lt;p&gt;Create a consumer class and annotate methods for events you want to handle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@EventConsumer&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserEventsConsumerV1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onCreated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventV1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserCreatedV1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Handle me&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onStatusUpdated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventV1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserStatusUpdatedV1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Handle me&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! The service automatically receives events regardless of the transport mechanism (Kafka, RabbitMQ, SQS).&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This hybrid event sourcing approach has proven highly effective in production systems I worked. It provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Audit trail completeness&lt;/strong&gt; without operational complexity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event-driven integration&lt;/strong&gt; without pure event sourcing overhead&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conflict resolution&lt;/strong&gt; that matches business requirements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer productivity&lt;/strong&gt; with familiar patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key insight is that you don't need pure event sourcing to get most of its benefits. By combining current state storage with comprehensive event logging, I achieve the best of both worlds: operational simplicity and event-driven architecture benefits.&lt;/p&gt;

&lt;p&gt;The framework handles the complexity of event processing, message broker integration, and version management, letting developers focus on business logic rather than infrastructure concerns.&lt;/p&gt;

&lt;p&gt;For teams considering event sourcing, I highly recommend exploring hybrid approaches. You might find, as I did, that the benefits are compelling while the operational burden remains manageable.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>eventdriven</category>
      <category>eventsourcing</category>
      <category>backend</category>
    </item>
    <item>
      <title>VibeTDD Experiment 4.7: Commons and Examples Compilation - Making AI Smarter Through Better Documentation</title>
      <dc:creator>Maksim Matlakhov</dc:creator>
      <pubDate>Mon, 15 Sep 2025 13:51:55 +0000</pubDate>
      <link>https://forem.com/maksim_matlakhov/vibetdd-experiment-47-commons-and-examples-compilation-making-ai-smarter-through-better-426i</link>
      <guid>https://forem.com/maksim_matlakhov/vibetdd-experiment-47-commons-and-examples-compilation-making-ai-smarter-through-better-426i</guid>
      <description>&lt;h2&gt;
  
  
  The Documentation Efficiency Problem
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://dev.to/maksim_matlakhov/phase-46-breaking-the-monolith-a-strategic-repository-split-2ghk"&gt;Phase 4.6&lt;/a&gt;, I split the monolithic repository into modular commons libraries to prevent AI from touching unwanted code. While this solved the physical boundaries problem, it created a new challenge: &lt;strong&gt;AI doesn't inherently know about custom libraries&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Asking Claude to read JAR files or external documentation disrupts the development workflow. At the same time, providing full commons code to AI is inefficient - it's a heavy task that includes implementation details AI doesn't need for most operations.&lt;/p&gt;

&lt;p&gt;The solution became clear: &lt;strong&gt;compile commons signatures and examples into AI-friendly documentation&lt;/strong&gt; that provides exactly what AI needs, when it needs it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Compilation Strategy
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What AI Actually Needs
&lt;/h3&gt;

&lt;p&gt;Through experimentation, I discovered AI needs different levels of detail for different tasks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For Commons Libraries&lt;/strong&gt;: Class signatures, method interfaces, and comments - but not implementation details. AI needs to understand what's available and how to use it, not how it works internally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For Implementation Examples&lt;/strong&gt;: Full code with all implementation details, comments, and test scenarios. When AI is building similar functionality, it needs complete patterns to follow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For Constants and Configuration&lt;/strong&gt;: All possible values and their meanings. These are reference data AI should have complete access to.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Compilation Projects
&lt;/h3&gt;

&lt;p&gt;I created two key repositories to handle this documentation strategy:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://gitlab.com/vibetdd/framework/be/kotlin/prompts/-/tree/main/compilation" rel="noopener noreferrer"&gt;Framework Prompts&lt;/a&gt;&lt;/strong&gt;: Contains compilation instructions for different types of documentation, split by purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;commons.md&lt;/code&gt;: Instructions for compiling commons libraries to signatures only&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;service-impl-main.md&lt;/code&gt;: Full implementation examples for AI to follow&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;service-impl-test.md&lt;/code&gt;: Complete test scenarios and patterns&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;service-objects-main.md&lt;/code&gt;: Object structures with empty method stubs for TDD&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;service-objects-test.md&lt;/code&gt;: Test utilities, fakes, and object mothers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://gitlab.com/vibetdd/framework/be/kotlin/examples" rel="noopener noreferrer"&gt;Framework Examples&lt;/a&gt;&lt;/strong&gt;: Stores the compiled markdown files that AI can easily read and reference during development.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Compilation Process in Action
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Commons Compilation Example
&lt;/h3&gt;

&lt;p&gt;Here's how commons compilation transforms verbose library code into AI-friendly documentation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before (Full Implementation)&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * Validates commands using a list of validation rules with async execution.
 * Supports parallel validation for improved performance.
 */&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CommandValidator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ValidationRule&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;,&lt;/span&gt;
    &lt;span class="n"&gt;coroutines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;validationDispatcher&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Dispatchers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limitedParallelism&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;parallelism&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;coroutines&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"command-validator"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="cm"&gt;/**
     * Validates command and throws ValidationException if errors found
     */&lt;/span&gt;
    &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;errors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validateAndGetErrors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNotEmpty&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nc"&gt;ValidationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="cm"&gt;/**
     * Validates command and returns list of validation errors without throwing
     */&lt;/span&gt;
    &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;validateAndGetErrors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ValidationError&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;validationDispatcher&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;rules&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isApplicable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validateOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;awaitAll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filterNotNull&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Suppress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"UNCHECKED_CAST"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;ValidationRule&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;*&amp;gt;.&lt;/span&gt;&lt;span class="nf"&gt;validateOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;ValidationError&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Any&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toInput&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;typedRule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ValidationRule&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;ValidationRule&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;typedRule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After (Compiled for AI)&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * Validates commands using a list of validation rules with async execution.
 * Supports parallel validation for improved performance.
 */&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CommandValidator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ValidationRule&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;,&lt;/span&gt;
    &lt;span class="n"&gt;coroutines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/**
     * Validates command and throws ValidationException if errors found
     */&lt;/span&gt;
    &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="cm"&gt;/**
     * Validates command and returns list of validation errors without throwing
     */&lt;/span&gt;
    &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;validateAndGetErrors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ValidationError&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compilation process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Extracts class signatures and public interfaces&lt;/li&gt;
&lt;li&gt;Preserves all comments (critical for AI understanding)&lt;/li&gt;
&lt;li&gt;Removes implementation details that AI doesn't need&lt;/li&gt;
&lt;li&gt;Groups by package for easy navigation&lt;/li&gt;
&lt;li&gt;Includes all constants with their values&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Examples Compilation Strategy
&lt;/h3&gt;

&lt;p&gt;For implementation examples, the strategy varies by purpose:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;service-objects-main.md&lt;/code&gt;: Prepared object structures with empty methods ready for TDD&lt;br&gt;&lt;br&gt;
&lt;code&gt;service-objects-test.md&lt;/code&gt;: Utility classes, fakes, and object mothers for testing&lt;br&gt;&lt;br&gt;
&lt;code&gt;service-impl-test.md&lt;/code&gt;: Full test scenarios showing expected patterns&lt;br&gt;&lt;br&gt;
&lt;code&gt;service-impl-main.md&lt;/code&gt;: Complete working implementations that AI can learn from  &lt;/p&gt;

&lt;p&gt;Each has specific instructions about what to include, what to remove, and how to format the results for maximum AI comprehension.&lt;/p&gt;
&lt;h4&gt;
  
  
  Service Objects Compilation Example
&lt;/h4&gt;

&lt;p&gt;The &lt;code&gt;service-objects-main.md&lt;/code&gt; compilation transforms complete implementations into TDD-ready structures:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before (Full Implementation)&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateUserUseCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;timeProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TimeProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;idProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IdProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;validator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CommandValidator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;storagePort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UserStoragePort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SaveCommandUseCase&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CreateUserCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="cm"&gt;/**
     * This is an entry point. Always implements SaveCommandUseCase&amp;lt;COMMAND, MODEL&amp;gt;
     */&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CreateUserCommand&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="c1"&gt;// NOTE: Carefully read requirements how to generate id: a random one or based on params&lt;/span&gt;
            &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;createdAt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;timeProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;updatedAt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;timeProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;storagePort&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After (Compiled for TDD)&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateUserUseCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;timeProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TimeProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;idProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IdProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;validator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CommandValidator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;storagePort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UserStoragePort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SaveCommandUseCase&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CreateUserCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="cm"&gt;/**
     * This is an entry point. Always implements SaveCommandUseCase&amp;lt;COMMAND, MODEL&amp;gt;
     */&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CreateUserCommand&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;TODO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Not Implemented Yet"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This compilation approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Preserves class structure and dependencies&lt;/li&gt;
&lt;li&gt;Keeps all comments that guide AI understanding&lt;/li&gt;
&lt;li&gt;Replaces implementation with appropriate stubs (&lt;code&gt;TODO&lt;/code&gt;, &lt;code&gt;null&lt;/code&gt;, &lt;code&gt;emptyList()&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Maintains type signatures for compilation success&lt;/li&gt;
&lt;li&gt;Provides implementation hints through comments&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Automation Solution
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Project-Level Scripts
&lt;/h3&gt;

&lt;p&gt;Each project gets simple bash scripts that execute Claude with the appropriate compilation prompt:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For Commons Projects&lt;/strong&gt; (example: &lt;a href="https://gitlab.com/vibetdd/framework/be/kotlin/libs/common-service/-/tree/main/scripts" rel="noopener noreferrer"&gt;common-service scripts&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../common-service-core &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit
&lt;/span&gt;claude &lt;span class="s2"&gt;"Read prompt: ~/VTProjects/framework/be/kotlin/prompts/compilation/commons.md"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;For Example Projects&lt;/strong&gt; (example: &lt;a href="https://gitlab.com/vibetdd/examples/be/kotlin/services/users/-/tree/main/scripts/compilation" rel="noopener noreferrer"&gt;users service scripts&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../../../../service/core/impl &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit
&lt;/span&gt;claude &lt;span class="s2"&gt;"Read prompt: ~/VTProjects/framework/be/kotlin/prompts/compilation/service-impl-main.md"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Compilation Workflow
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Developer updates commons library or example code&lt;/li&gt;
&lt;li&gt;Runs compilation script (single bash command)&lt;/li&gt;
&lt;li&gt;Claude reads the prompt, extracts appropriate information&lt;/li&gt;
&lt;li&gt;Result is automatically placed in the examples project&lt;/li&gt;
&lt;li&gt;AI can now reference the compiled documentation efficiently&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Integration with Development Workflow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Before Code Generation
&lt;/h3&gt;

&lt;p&gt;Now when I ask AI to generate code based on specs, the process starts with:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read compiled commons&lt;/strong&gt;: AI understands available utilities and patterns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read relevant examples&lt;/strong&gt;: AI sees complete implementation patterns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate following patterns&lt;/strong&gt;: AI has concrete examples to follow&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Results So Far
&lt;/h3&gt;

&lt;p&gt;The compiled documentation approach shows promising improvements:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Faster Context Loading&lt;/strong&gt;: AI can quickly scan compiled signatures instead of parsing full implementations&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Better Pattern Recognition&lt;/strong&gt;: Complete examples help AI generate more accurate code&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Reduced Invention&lt;/strong&gt;: When AI sees established patterns, it's less likely to invent complex solutions for simple problems&lt;/p&gt;

&lt;h3&gt;
  
  
  Current Limitations
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Still Need More Examples&lt;/strong&gt;: AI generates better results with more reference examples. So far I provided just one example with &lt;a href="https://gitlab.com/vibetdd/examples/be/kotlin/services/users" rel="noopener noreferrer"&gt;users&lt;/a&gt; service, that's not enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test Modification Behavior&lt;/strong&gt;: AI still occasionally modifies tests when implementation has issues, instead of fixing the implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps: Beyond CRUD
&lt;/h2&gt;

&lt;p&gt;The current system I built assumes basic CRUD operations, but my architecture requires event-driven patterns that are still missing. The next phase I will fix it and add events support.&lt;/p&gt;

&lt;p&gt;I'm also investigating &lt;strong&gt;Event Modeling&lt;/strong&gt; as a framework that might naturally align with my architecture goals and make the specification-to-implementation process more seamless.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Insight: Documentation as Code Interface
&lt;/h2&gt;

&lt;p&gt;This phase taught me that &lt;strong&gt;documentation for AI isn't the same as documentation for humans&lt;/strong&gt;. AI needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Structured, consistent formatting&lt;/li&gt;
&lt;li&gt;Complete examples, not partial snippets&lt;/li&gt;
&lt;li&gt;Signatures without implementation noise&lt;/li&gt;
&lt;li&gt;Reference data in easily scannable format&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The compilation approach treats documentation as an &lt;strong&gt;interface between human-written code and AI consumption&lt;/strong&gt; - optimized for machine reading while remaining human-understandable.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This compilation system demonstrates another VibeTDD principle: optimize the AI's context for the specific task at hand, rather than providing everything and hoping AI figures out what's relevant.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>tdd</category>
      <category>vibecoding</category>
      <category>vibetdd</category>
    </item>
    <item>
      <title>Beyond the Monolith vs Microservices Debate: A Practical Guide to Deployment-Agnostic Services</title>
      <dc:creator>Maksim Matlakhov</dc:creator>
      <pubDate>Tue, 09 Sep 2025 12:40:30 +0000</pubDate>
      <link>https://forem.com/maksim_matlakhov/beyond-the-monolith-vs-microservices-debate-a-practical-guide-to-deployment-agnostic-services-3l41</link>
      <guid>https://forem.com/maksim_matlakhov/beyond-the-monolith-vs-microservices-debate-a-practical-guide-to-deployment-agnostic-services-3l41</guid>
      <description>&lt;h2&gt;
  
  
  The Problem with Choices
&lt;/h2&gt;

&lt;p&gt;The monolith vs microservices debate forces teams into a false choice that constrains both development and deployment options. Many teams want to move toward distributed systems but find themselves trapped by poorly designed monoliths where components are tightly coupled and difficult to extract without comprehensive test coverage. Others adopt microservices prematurely and struggle with operational complexity when their applications could run perfectly well as monoliths.&lt;/p&gt;

&lt;p&gt;The solution isn't choosing sides - it's building services that can deploy either way through configuration, not architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building on Modular Foundations
&lt;/h2&gt;

&lt;p&gt;This post builds on the modular architecture established in &lt;a href="https://dev.to/maksim_matlakhov/phase-46-breaking-the-monolith-a-strategic-repository-split-2ghk"&gt;Phase 4.6: Breaking the Monolith&lt;/a&gt;, where we split repositories into parent POMs, commons libraries, and service modules. That phase created physical boundaries to prevent AI from modifying files it shouldn't touch. Now we add logical boundaries through deployment flexibility.&lt;/p&gt;

&lt;p&gt;The same service code can run embedded within a larger application or as an independent server, controlled purely by configuration. This approach eliminates the need to commit to architectural extremes upfront.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Implementation Strategy
&lt;/h2&gt;

&lt;p&gt;The key insight is &lt;a href="https://gitlab.com/vibetdd/examples/be/kotlin/services/users/-/commit/8745714315298331f71fe503ca85ababb1befc00" rel="noopener noreferrer"&gt;separating service logic&lt;/a&gt; from its deployment mode. We'll transform the users service from our modular architecture by adding three layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Service Application Module&lt;/strong&gt;: HTTP interface for standalone deployment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;REST Client Implementation&lt;/strong&gt;: Network-based client that mirrors internal client interface&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configuration-Driven Selection&lt;/strong&gt;: Spring profiles that choose deployment mode&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's walk through each step with real implementation examples.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Service Application Module
&lt;/h3&gt;

&lt;p&gt;The service-app module (previously used only for acceptance testing) now provides the HTTP layer for external access. Each service needs a unique local port for separate local run:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;application-local.yml&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;server.port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8000&lt;/span&gt;  &lt;span class="c1"&gt;# Unique port for this service&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Spring Boot Application:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@SpringBootApplication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scanBasePackages&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"vt.demo.users"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"dev.vibetdd.service.api.common.rest"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Application&lt;/span&gt;

&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;runApplication&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;UserController Implementation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@RestController&lt;/span&gt;
&lt;span class="nd"&gt;@RequestMapping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/v1/users"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserControllerV1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;userCoreFactory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UserCoreFactory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@PostMapping&lt;/span&gt;
    &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nd"&gt;@RequestBody&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CreateUserParamsV1&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;ModelV1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserV1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userCoreFactory&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;createUseCase&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toCommand&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toV1&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="nd"&gt;@PutMapping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{id}/versions/{version}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nd"&gt;@PathVariable&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nd"&gt;@PathVariable&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nd"&gt;@RequestBody&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UpdateUserParamsV1&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;ModelV1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserV1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userCoreFactory&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updateUseCase&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toV1&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;// ... other methods&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how the controller uses the same &lt;code&gt;UserCoreFactory&lt;/code&gt; that internal clients use - no duplication of business logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: REST Client Implementation
&lt;/h3&gt;

&lt;p&gt;Create a REST client that implements the same interface as the internal client:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UsersRestClient.kt:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UsersRestClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UsersRestClientProps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;httpClient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;HttpClient&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UsersClientV1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CreateUserRequestV1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;ModelV1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserV1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;clientCall&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;httpClient&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/v1/users"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;setBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;timeout&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;requestTimeoutMillis&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTimeoutRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UpdateUserRequestV1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;ModelV1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserV1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;clientCall&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;httpClient&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/v1/users/${request.id}/versions/${request.version}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;setBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;timeout&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;requestTimeoutMillis&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTimeoutRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// ... other methods&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Configuration-Driven Client Selection
&lt;/h3&gt;

&lt;p&gt;The crucial change is switching the default from internal to REST client. Notice how &lt;code&gt;matchIfMissing&lt;/code&gt; moved:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before - Internal Client as Default:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@ConditionalOnProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"clients.users.type"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;havingValue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"INTERNAL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;matchIfMissing&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;  &lt;span class="c1"&gt;// Was the default&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InternalClientConfig&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After - REST Client as Default:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Internal Client Configuration&lt;/span&gt;
&lt;span class="nd"&gt;@ConditionalOnProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"clients.users.type"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;havingValue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"INTERNAL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;matchIfMissing&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;  &lt;span class="c1"&gt;// No longer default&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InternalClientConfig&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// REST Client Configuration  &lt;/span&gt;
&lt;span class="nd"&gt;@ConditionalOnProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"clients.users.type"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;havingValue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"REST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;matchIfMissing&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;  &lt;span class="c1"&gt;// Now the default&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RestClientConfig&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single change transforms the entire deployment model - services now default to distributed mode while preserving monolithic capability.&lt;/p&gt;

&lt;h4&gt;
  
  
  Build Configuration Strategy
&lt;/h4&gt;

&lt;p&gt;The Maven setup enables different dependencies for different deployment modes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client Module Dependencies:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- users-client-spring/pom.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dependencies&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;dev.vibetdd.demo.service&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;users-client-rest&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;  &lt;span class="c"&gt;&amp;lt;!-- Always included --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;dev.vibetdd.demo.service&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;users-client-internal&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;scope&amp;gt;&lt;/span&gt;provided&lt;span class="nt"&gt;&amp;lt;/scope&amp;gt;&lt;/span&gt;  &lt;span class="c"&gt;&amp;lt;!-- Only for local development, should be provided by consumers (api-admin in our case) --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependencies&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Seamless Testing
&lt;/h3&gt;

&lt;p&gt;Simply add the new value &lt;code&gt;ClientType.REST&lt;/code&gt; and extend factory, and all existing tests automatically validate both implementations:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TestUsersClientFactory.kt:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ClientType&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;INTERNAL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;REST&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TestUsersClientFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clientType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ClientType&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;UsersClientV1&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clientType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;ClientType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;INTERNAL&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;UsersInternalClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.)&lt;/span&gt;
        &lt;span class="nc"&gt;ClientType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;REST&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;UsersRestClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="c1"&gt;// Configure the new client&lt;/span&gt;
            &lt;span class="n"&gt;props&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;restClientProps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;httpClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HttpClientFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;props&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;restClientProps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;objectMapper&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;objectMapper&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every acceptance test runs against both implementations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@ParameterizedTest&lt;/span&gt;
&lt;span class="nd"&gt;@EnumSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ClientType&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;`should&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="nf"&gt;successfully`&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clientType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ClientType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;testUsersClientFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clientType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;// Test automatically validates both deployment modes&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Consumer Integration: &lt;a href="https://gitlab.com/vibetdd/examples/be/kotlin/services/api/-/commit/bc3751ef91a958a03fc66fff408884a151876610" rel="noopener noreferrer"&gt;The API Layer&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;The consuming applications require minimal changes - just dependency version updates:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maven Dependencies:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Parent pom.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;properties&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- Update the version (or let maven versions plugin to do it) --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;client.users.version&amp;gt;&lt;/span&gt;1.1.0&lt;span class="nt"&gt;&amp;lt;/client.users.version&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/properties&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Local Development Configuration:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# application-local.yml&lt;/span&gt;
&lt;span class="na"&gt;clients&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;users&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${CLIENT_USERS_TYPE:INTERNAL}&lt;/span&gt;  &lt;span class="c1"&gt;# Monolith mode for development&lt;/span&gt;
    &lt;span class="na"&gt;rest.url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:8000&lt;/span&gt;  &lt;span class="c1"&gt;# Set the service port if you want to switch to REST mode&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And Maven Config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!--api-admin/pom.xml--&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dependencies&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- Let client config to set the implementation --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;vt.demo.service&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;users-client-spring&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- Always include internal for testing --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;vt.demo.service&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;users-client-internal&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;scope&amp;gt;&lt;/span&gt;test&lt;span class="nt"&gt;&amp;lt;/scope&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependencies&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;profiles&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;profile&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;id&amp;gt;&lt;/span&gt;local-dev&lt;span class="nt"&gt;&amp;lt;/id&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;dependencies&amp;gt;&lt;/span&gt;
            &lt;span class="c"&gt;&amp;lt;!-- Include internal for local development --&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;vt.demo.service&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;users-client-internal&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/dependencies&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/profile&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/profiles&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For production, no configuration is needed - the service auto-configures to REST mode and discovers the service URL automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing Configuration:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# application-test.yml&lt;/span&gt;
&lt;span class="na"&gt;clients&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;users&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;INTERNAL&lt;/span&gt;  &lt;span class="c1"&gt;# Always internal, the testing will continue working with the real service logic&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Deployment Modes
&lt;/h2&gt;

&lt;p&gt;This configuration gives you three deployment options:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Full Monolith
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;clients&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;users&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;INTERNAL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything runs in one JVM, zero network calls, instant startup.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Hybrid Mode
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;clients&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;users&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REST&lt;/span&gt;  &lt;span class="c1"&gt;# Users service separate&lt;/span&gt;
  &lt;span class="na"&gt;orders&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;INTERNAL&lt;/span&gt;  &lt;span class="c1"&gt;# Orders service embedded&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mix standalone and embedded services based on scaling needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Full Microservices
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# No configuration needed - REST is now default&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All services running independently with automatic service discovery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Benefits
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Zero Test Disruption
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Existing acceptance tests automatically validate both implementations&lt;/li&gt;
&lt;li&gt;No mocking required - tests use real services with real databases&lt;/li&gt;
&lt;li&gt;Same test suite gives confidence in both deployment modes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Development Simplicity
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Local development runs as monolith - no Docker or manual services run complexity&lt;/li&gt;
&lt;li&gt;Instant startup times, easy debugging across service boundaries&lt;/li&gt;
&lt;li&gt;Change one property to test against standalone service&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Deployment Flexibility
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Extract services one at a time based on actual scaling needs&lt;/li&gt;
&lt;li&gt;Easy rollback by changing configuration&lt;/li&gt;
&lt;li&gt;No pressure to migrate everything at once&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Gradual Migration Path
&lt;/h3&gt;

&lt;p&gt;Teams can start with monolithic deployment and gradually extract services as operational expertise and infrastructure mature. The same codebase supports both approaches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond the Debate
&lt;/h2&gt;

&lt;p&gt;This approach resolves the monolith vs microservices debate by making it irrelevant. Instead of choosing sides, you build services that adapt to your needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;When you need simplicity&lt;/strong&gt;: Deploy as monolith&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When you need scale&lt;/strong&gt;: Deploy as microservices&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When you need both&lt;/strong&gt;: Deploy hybrid mode&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The architecture enables possibilities rather than constraining them. Whether you need the simplicity of a monolith or the scalability of microservices isn't a permanent decision - it's a runtime configuration choice.&lt;/p&gt;

&lt;p&gt;Teams no longer need to commit to architectural extremes upfront. They can start simple and evolve complexity only when business requirements demand it, all while maintaining the same codebase and test suite.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This approach demonstrates that good architecture makes hard things easy and easy things trivial. The hardest part of microservices shouldn't be running them locally for development, and the hardest part of monoliths shouldn't be extracting services when you need to scale.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>microservices</category>
      <category>architecture</category>
      <category>kotlin</category>
      <category>springboot</category>
    </item>
    <item>
      <title>Phase 4.6: Breaking the Monolith - A Strategic Repository Split</title>
      <dc:creator>Maksim Matlakhov</dc:creator>
      <pubDate>Wed, 03 Sep 2025 13:35:25 +0000</pubDate>
      <link>https://forem.com/maksim_matlakhov/phase-46-breaking-the-monolith-a-strategic-repository-split-2ghk</link>
      <guid>https://forem.com/maksim_matlakhov/phase-46-breaking-the-monolith-a-strategic-repository-split-2ghk</guid>
      <description>&lt;h2&gt;
  
  
  The Decision to Evolve
&lt;/h2&gt;

&lt;p&gt;Following the insights from Phase 4.5, where I discovered that &lt;a href="https://dev.to/maksim_matlakhov/phase-45-spec-as-source-of-truth-rethinking-development-workflow-40om"&gt;specs should reflect reality, not intentions&lt;/a&gt;, Phase 4.6 emerged as the natural next step. The previous phases had taught me critical lessons about AI limitations and the need for &lt;strong&gt;physical boundaries over prompts&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;After progressing through experiments with convention generation, project setup automation, and AI-assisted TDD workflows, it became clear that my monolithic repository structure was creating unnecessary friction. When AI has access to everything, it tends to modify files it shouldn't touch. The solution wasn't better prompts - it was better architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The New Architecture: From One to Many
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Foundation Layer: Parent POMs
&lt;/h3&gt;

&lt;p&gt;The split began with establishing solid foundations through two distinct parent POM structures:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;General Parent POM&lt;/strong&gt; (&lt;a href="https://gitlab.com/vibetdd/framework/be/kotlin/parent-pom" rel="noopener noreferrer"&gt;parent-pom&lt;/a&gt;): The cornerstone of my framework, this repository defines all actual versions of commonly used dependencies. Following the same approach as Spring Boot's dependency management, updating versions across all projects becomes as simple as updating the parent POM version in child projects. I also leverage the Maven versions plugin to automatically update minor versions of dependencies during the build process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spring-Specific Parent POM&lt;/strong&gt; (&lt;a href="https://gitlab.com/vibetdd/framework/be/kotlin/parent-pom-spring" rel="noopener noreferrer"&gt;parent-pom-spring&lt;/a&gt;): This specialized parent extends the general POM with Spring-specific configurations, dependencies, and best practices. It also defines the dependencies for the common libraries described below in this post. This separation allows non-Spring projects to remain lightweight while Spring projects get everything they need out-of-the-box, all with centralized version management.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Libraries: The Commons Repositories
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://gitlab.com/vibetdd/framework/be/kotlin/libs" rel="noopener noreferrer"&gt;libs repository&lt;/a&gt; became my central hub for shared, domain-agnostic functionality. This collection of common libraries eliminates code duplication and provides consistent patterns across all projects:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://gitlab.com/vibetdd/framework/be/kotlin/libs/common-utils" rel="noopener noreferrer"&gt;common-utils&lt;/a&gt;&lt;/strong&gt;: Framework and domain-agnostic utility functions including Kotlin extension functions and other utilities like UUID generation. These are the building blocks that every project needs but shouldn't have to implement from scratch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://gitlab.com/vibetdd/framework/be/kotlin/libs/common-testing" rel="noopener noreferrer"&gt;common-testing&lt;/a&gt;&lt;/strong&gt;: Testing infrastructure including the Rand object for generating random test values (developed in previous phases), Spring testing configurations, and Docker container setup helpers. This library standardizes my testing approach across all services.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://gitlab.com/vibetdd/framework/be/kotlin/libs/common-dto" rel="noopener noreferrer"&gt;common-dto&lt;/a&gt;&lt;/strong&gt;: Universal data transfer objects applicable to any project and domain. Contains foundational DTOs like ModelV1, PageV1, and PageQueryV1 that establish consistent API patterns across the entire ecosystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://gitlab.com/vibetdd/framework/be/kotlin/libs/common-service" rel="noopener noreferrer"&gt;common-service&lt;/a&gt;&lt;/strong&gt;: Organized into modules by service layer (api, client, domain), this library provides domain-agnostic infrastructure including common exception objects and handlers, base models, mappers, and validators that form the backbone of my hexagonal architecture implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Practical Implementation: The Users API Demo
&lt;/h3&gt;

&lt;p&gt;To validate my new architecture and provide concrete examples, I created a comprehensive &lt;a href="https://gitlab.com/vibetdd/examples/be/kotlin/services" rel="noopener noreferrer"&gt;users API project&lt;/a&gt; that showcases how all these pieces work together in practice. It demonstrates my framework in action through a complete user management system built on the new modular architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  Users Service: TDD-Built with Hexagonal Architecture
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://gitlab.com/vibetdd/examples/be/kotlin/services/users" rel="noopener noreferrer"&gt;users service&lt;/a&gt; was built using Test-Driven Development with behavior tests. While this step was done manually, it serves as an excellent example for future AI-assisted development. The service implements a modular Maven project with three main modules following hexagonal architecture principles:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Service Module&lt;/strong&gt;: Contains submodules organized by hexagonal architecture layers (domain, application, infrastructure), ensuring clean separation between business logic and external concerns. This structure makes the codebase more testable and maintainable while providing clear boundaries for AI to understand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client Module&lt;/strong&gt;: Provides a clean interface for communication with the users service. This module contains submodules with the API interface, different implementations (currently internal-only, but designed to support REST, WebSockets, and other protocols), and Spring configuration for seamless integration into other services.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DTO Module&lt;/strong&gt;: Houses common data transfer objects shared between the service and client modules, ensuring consistent data contracts and preventing duplication of model definitions.&lt;/p&gt;

&lt;h3&gt;
  
  
  API Layer: Integration With Public World
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://gitlab.com/vibetdd/examples/be/kotlin/services/api" rel="noopener noreferrer"&gt;API component&lt;/a&gt; serves as the gateway and includes the entire users service as a dependency - not just the client, but the whole service. This creates a monolithic deployment while maintaining modular development, giving us the best of both worlds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Current Module Structure&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Specialized API Modules&lt;/strong&gt;: Organized by purpose and audience (api-admin, api-public, api-mobile), each module can be tailored for specific use cases with appropriate security, rate limiting, and feature sets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App Module&lt;/strong&gt;: Runs the entire application as a single deployable unit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Testing Philosophy&lt;/strong&gt;: This layer tests only its specific responsibilities - ensuring controller endpoints are properly designed and input validation works correctly. There are no mocks; tests call the real users service with a real database. The tests remain remarkably simple (&lt;a href="https://gitlab.com/vibetdd/examples/be/kotlin/services/api/-/tree/main/api-admin/src/test/kotlin/vt/demo/api/admin/users" rel="noopener noreferrer"&gt;see the actual tests&lt;/a&gt;). For testing, I use Ktor client instead of traditional MockMvc because MockMvc is complex and unpredictable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Future Scaling Strategy&lt;/strong&gt;: The modular design allows individual API modules to be extracted into standalone servers later, enabling horizontal scaling and team-specific deployment cycles as the system grows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Split Matters
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Modularity and Reusability
&lt;/h3&gt;

&lt;p&gt;Each repository now has a clear, single responsibility. The parent POMs can be reused across any number of projects, the commons libraries provide consistent functionality, and the users API shows how everything integrates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Independent Development Cycles
&lt;/h3&gt;

&lt;p&gt;Different components can now be developed independently. Changes to the commons libraries don't require touching the parent POMs, and new services can be created without affecting existing ones.&lt;/p&gt;

&lt;h3&gt;
  
  
  Centralized Version Management
&lt;/h3&gt;

&lt;p&gt;With parent POMs managing all dependency versions and the Maven versions plugin automatically handling minor updates, version conflicts become a thing of the past. A single parent POM update propagates consistently across the entire ecosystem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scalability
&lt;/h3&gt;

&lt;p&gt;This structure naturally supports the addition of new services, libraries, and configurations as the framework grows. Each new component finds its appropriate place within the established architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking Forward
&lt;/h2&gt;

&lt;p&gt;With the repository structure in place, I'm going to create templates for both the service and API layers. Then I'll continue my experiments with AI to generate tests and implementations.&lt;/p&gt;

&lt;p&gt;However, a quick test revealed an issue: having commons libraries creates overhead for AI. Claude (and other AI models) don't inherently know about these custom libraries, and asking AI to check JAR files or external documentation disrupts the workflow.&lt;/p&gt;

&lt;p&gt;The solution is to build a knowledge base and compile it into a simple format that AI can understand - making the commons functionality discoverable without breaking the development flow.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This repository split demonstrates a core VibeTDD principle: when working with AI, make good behaviors easy and bad behaviors impossible through architecture, not instructions.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>tdd</category>
      <category>vibecoding</category>
      <category>vibetdd</category>
    </item>
    <item>
      <title>Phase 4.5: Spec as Source of Truth - Rethinking Development Workflow</title>
      <dc:creator>Maksim Matlakhov</dc:creator>
      <pubDate>Tue, 26 Aug 2025 08:50:52 +0000</pubDate>
      <link>https://forem.com/maksim_matlakhov/phase-45-spec-as-source-of-truth-rethinking-development-workflow-40om</link>
      <guid>https://forem.com/maksim_matlakhov/phase-45-spec-as-source-of-truth-rethinking-development-workflow-40om</guid>
      <description>&lt;p&gt;After stepping back from the &lt;a href="https://dev.to/maksim_matlakhov/vibetdd-experiment-44-storage-layer-testing-and-the-never-give-up-problem-5f1g"&gt;"I will never give up"&lt;/a&gt; AI issue, I decided to tackle a more fundamental problem: the sync between spec-story-task-test. The question became: which part should be the authoritative source for requirements, examples, API specs, and how do we manage spec updates without creating chaos?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Current Workflow
&lt;/h2&gt;

&lt;p&gt;From my experience, here's how development typically works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;PO builds a document&lt;/strong&gt; with requirements (or skips to the next step)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split to stories&lt;/strong&gt;: Either common stories or platform-specific stories (backend, web, mobile)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developers create dev tasks&lt;/strong&gt; (or work directly under stories)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Implementation begins&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changes happen during development&lt;/strong&gt; - but requirements/stories don't always get updated&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unsync occurs&lt;/strong&gt;: If no spec exists, stories (or code) become the source of truth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change requests create confusion&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;PO updates spec and creates new stories&lt;/li&gt;
&lt;li&gt;If spec is outdated, changes apply to irrelevant source&lt;/li&gt;
&lt;li&gt;If spec gets updated but changes aren't implemented, PO forgets to revert&lt;/li&gt;
&lt;li&gt;Nobody knows current functionality status when change requests are in progress&lt;/li&gt;
&lt;li&gt;If there is spec versioning then it adds complexity and mess&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  My Proposed Solution: Story-First, Spec-Last Workflow
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Story Creation&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PO creates a &lt;strong&gt;master story&lt;/strong&gt; with requirements&lt;/li&gt;
&lt;li&gt;Describes which components need changes: backend, web, mobile&lt;/li&gt;
&lt;li&gt;Uses structured templates designed for both humans and AI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;b&gt;Example: Master Story for Create Payout&lt;/b&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Story: Create Payout - Submit New Payout Request&lt;/span&gt;

&lt;span class="gu"&gt;## Story Overview&lt;/span&gt;
&lt;span class="gs"&gt;**As a**&lt;/span&gt; user  
&lt;span class="gs"&gt;**I want to**&lt;/span&gt; create a new payout request  
&lt;span class="gs"&gt;**So that**&lt;/span&gt; I can receive funds to my account

&lt;span class="gu"&gt;## User Acceptance Criteria&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; &lt;span class="gs"&gt;**Given**&lt;/span&gt; I have valid request params &lt;span class="gs"&gt;**When**&lt;/span&gt; I submit a payout request with valid details &lt;span class="gs"&gt;**Then**&lt;/span&gt; the payout is created successfully
&lt;span class="p"&gt;2.&lt;/span&gt; &lt;span class="gs"&gt;**Given**&lt;/span&gt; I provide invalid or missing required fields &lt;span class="gs"&gt;**When**&lt;/span&gt; I submit the payout request &lt;span class="gs"&gt;**Then**&lt;/span&gt; I receive validation error messages
&lt;span class="p"&gt;3.&lt;/span&gt; &lt;span class="gs"&gt;**Given**&lt;/span&gt; my total payout amount would exceed the allowed limit &lt;span class="gs"&gt;**When**&lt;/span&gt; I submit the payout request &lt;span class="gs"&gt;**Then**&lt;/span&gt; I receive an error about exceeding limits

&lt;span class="gu"&gt;## Business Rules&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**UserId**&lt;/span&gt;: Must be provided and be a valid UUID format
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Amount**&lt;/span&gt;: Must be provided and must be in range of configured individual payout limit
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Currency**&lt;/span&gt;: Must be provided, must be valid ISO code, and must be one of configured allowed currencies
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**User Total Limit**&lt;/span&gt;: The sum of all payouts for the user must not exceed configured total limit

&lt;span class="gu"&gt;## Error Scenarios&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Missing UserId**&lt;/span&gt;: When UserId is not provided → User sees "User ID is required"
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Invalid UserId**&lt;/span&gt;: When UserId is not valid UUID format → User sees "Invalid User ID format"
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Missing Amount**&lt;/span&gt;: When Amount is not provided → User sees "Amount is required"
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Invalid Amount**&lt;/span&gt;: When Amount is out of range → User sees "Amount must be from {min} to {max}"
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Amount Limit Exceeded**&lt;/span&gt;: When Amount exceeds configured individual limit → User sees "Amount exceeds maximum allowed limit: {limit}"
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Missing Currency**&lt;/span&gt;: When Currency is not provided → User sees "Currency is required"
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Invalid Currency**&lt;/span&gt;: When Currency is not valid ISO code → User sees "Invalid currency code"
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Currency Not Allowed**&lt;/span&gt;: When Currency is not in configured allowed list → User sees "Currency not supported, supported currencies: {currencies}"
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Total Limit Exceeded**&lt;/span&gt;: When user's total payouts would exceed configured limit → User sees "Total payout limit {limit} exceeded for your account"

&lt;span class="gu"&gt;## Configuration Requirements&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Individual Payout Limit**&lt;/span&gt;: Min payout is 0.10 and Max is 30.00
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**User Total Limit**&lt;/span&gt;: Maximum total amount a user can have is 120
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Allowed Currencies**&lt;/span&gt;: List of supported ISO currency codes: EUR, USD, GBP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;2. Story Breakdown&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PO creates &lt;strong&gt;component-specific sub-stories&lt;/strong&gt; with help from dev teams&lt;/li&gt;
&lt;li&gt;Add high level technical details: acceptance criteria, API interfaces, data objects (in JSON)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;b&gt;Example: API Component Sub-Story&lt;/b&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# POST /v1/payouts - Create Payout&lt;/span&gt;

&lt;span class="gu"&gt;## Endpoint Definition&lt;/span&gt;
&lt;span class="gs"&gt;**Purpose**&lt;/span&gt;: Create new payout request based on user request  
&lt;span class="gs"&gt;**Method**&lt;/span&gt;: POST  
&lt;span class="gs"&gt;**Path**&lt;/span&gt;: /v1/payouts  
&lt;span class="gs"&gt;**Content-Type**&lt;/span&gt;: application/json

&lt;span class="gu"&gt;## Request Structure&lt;/span&gt;

&lt;span class="gu"&gt;### Request Body: CreatePayoutParamsV1&lt;/span&gt;
{
  "userId": "123e4567-e89b-12d3-a456-426614174000",
  "amount": 25.50,
  "currency": "USD"
}

&lt;span class="gu"&gt;## Input Validation&lt;/span&gt;
&lt;span class="gs"&gt;**Field-level validation**&lt;/span&gt; (format, required, type checking):
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**userId**&lt;/span&gt;: NotNull, UUID
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**amount**&lt;/span&gt;: NotNull, BigDecimal
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**currency**&lt;/span&gt;: NotNull, Pattern(ISO_CURRENCY_CODE)

&lt;span class="gu"&gt;## Business Validation&lt;/span&gt;
&lt;span class="gs"&gt;**Business rules**&lt;/span&gt; that apply during creation:
&lt;span class="p"&gt;
1.&lt;/span&gt; &lt;span class="gs"&gt;**Individual Payout Limit**&lt;/span&gt;: Amount must be within configured individual payout limits
&lt;span class="p"&gt;    -&lt;/span&gt; Test Scenarios:
&lt;span class="p"&gt;        -&lt;/span&gt; Configuration: minAmount = 0.10, maxAmount = 30.00
&lt;span class="p"&gt;        -&lt;/span&gt; givenAmount=0.10 → 201 Created
&lt;span class="p"&gt;        -&lt;/span&gt; givenAmount=30.00 → 201 Created
&lt;span class="p"&gt;        -&lt;/span&gt; givenAmount=0.09 → 422 AMOUNT_OUT_OF_RANGE
&lt;span class="p"&gt;        -&lt;/span&gt; givenAmount=30.01 → 422 AMOUNT_OUT_OF_RANGE
&lt;span class="p"&gt;
2.&lt;/span&gt; &lt;span class="gs"&gt;**User Total Limit**&lt;/span&gt;: The sum of all payouts for the user must not exceed configured total limit
&lt;span class="p"&gt;    -&lt;/span&gt; Test Scenarios:
&lt;span class="p"&gt;        -&lt;/span&gt; Configuration: userTotalLimit = 120.00
&lt;span class="p"&gt;        -&lt;/span&gt; userTotal=90.0, givenAmount=25.0 → 201 Created
&lt;span class="p"&gt;        -&lt;/span&gt; userTotal=0.0, givenAmount=119.99 → 201 Created
&lt;span class="p"&gt;        -&lt;/span&gt; userTotal=100.0, givenAmount=25.0 → 422 USER_TOTAL_LIMIT_EXCEEDED
&lt;span class="p"&gt;        -&lt;/span&gt; userTotal=115.0, givenAmount=10.0 → 422 USER_TOTAL_LIMIT_EXCEEDED
&lt;span class="p"&gt;
3.&lt;/span&gt; &lt;span class="gs"&gt;**Currency Support**&lt;/span&gt;: Currency must be one of configured allowed currencies
&lt;span class="p"&gt;    -&lt;/span&gt; Test Scenarios:
&lt;span class="p"&gt;        -&lt;/span&gt; Configuration: allowedCurrencies = ["EUR", "USD", "GBP"]
&lt;span class="p"&gt;        -&lt;/span&gt; givenCurrency=EUR → 201 Created
&lt;span class="p"&gt;        -&lt;/span&gt; givenCurrency=USD → 201 Created
&lt;span class="p"&gt;        -&lt;/span&gt; givenCurrency=GBP → 201 Created
&lt;span class="p"&gt;        -&lt;/span&gt; givenCurrency=JPY → 422 CURRENCY_NOT_SUPPORTED

&lt;span class="gu"&gt;## Success Response&lt;/span&gt;

&lt;span class="gu"&gt;### 201 Created&lt;/span&gt;
{
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "version": 1,
  "createdAt": "2025-08-20T10:30:00Z",
  "updatedAt": "2025-08-20T10:30:00Z",
  "data": {
    "userId": "123e4567-e89b-12d3-a456-426614174000",
    "amount": 25.50,
    "currency": "USD"
  }
}

&lt;span class="gu"&gt;## Error Responses&lt;/span&gt;

&lt;span class="gu"&gt;### 422 Unprocessable Entity - Missing UserId&lt;/span&gt;
{
  "errors": [
    {
      "code": "REQUIRED_FIELD_MISSING",
      "message": "User ID is required",
      "attributes": {
        "field": "userId"
      }
    }
  ]
}

&lt;span class="gu"&gt;### 422 Unprocessable Entity - Invalid UserId Format&lt;/span&gt;
{
  "errors": [
    {
      "code": "INVALID_FORMAT",
      "message": "Invalid User ID format",
      "attributes": {
        "field": "userId",
        "value": "invalid-uuid"
      }
    }
  ]
}

&lt;span class="gu"&gt;### 422 Unprocessable Entity - Missing Amount&lt;/span&gt;
{
  "errors": [
    {
      "code": "REQUIRED_FIELD_MISSING",
      "message": "Amount is required",
      "attributes": {
        "field": "amount"
      }
    }
  ]
}

&lt;span class="gu"&gt;### 422 Unprocessable Entity - Invalid Amount Range&lt;/span&gt;
{
  "errors": [
    {
      "code": "AMOUNT_OUT_OF_RANGE",
      "message": "Amount must be from {minAmount} to {maxAmount}",
      "attributes": {
        "field": "amount",
        "value": "0.05",
        "minAmount": "{minAmount}",
        "maxAmount": "{maxAmount}"
      }
    }
  ]
}

&lt;span class="gu"&gt;### 422 Unprocessable Entity - Missing Currency&lt;/span&gt;
{
  "errors": [
    {
      "code": "REQUIRED_FIELD_MISSING",
      "message": "Currency is required",
      "attributes": {
        "field": "currency"
      }
    }
  ]
}

&lt;span class="gu"&gt;### 422 Unprocessable Entity - Invalid Currency Code&lt;/span&gt;
{
  "errors": [
    {
      "code": "INVALID_CURRENCY_CODE",
      "message": "Invalid currency code",
      "attributes": {
        "field": "currency",
        "value": "INVALID"
      }
    }
  ]
}

&lt;span class="gu"&gt;### 422 Unprocessable Entity - Currency Not Supported&lt;/span&gt;
{
  "errors": [
    {
      "code": "CURRENCY_NOT_SUPPORTED",
      "message": "Currency not supported, supported currencies: {currencies}",
      "attributes": {
        "field": "currency",
        "value": "JPY",
        "supportedCurrencies": ["{currencies}"]
      }
    }
  ]
}

&lt;span class="gu"&gt;### 422 Unprocessable Entity - User Total Limit Exceeded&lt;/span&gt;
{
  "errors": [
    {
      "code": "USER_TOTAL_LIMIT_EXCEEDED",
      "message": "Total payout limit {limit} exceeded for your account",
      "attributes": {
        "field": "amount",
        "requestedAmount": "25.00",
        "currentTotal": "100.00",
        "totalLimit": "{limit}",
        "wouldExceedBy": "5.00"
      }
    }
  ]
}

&lt;span class="gu"&gt;## External Service Dependencies&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Configuration Service**&lt;/span&gt;: Used for retrieving payout limits and supported currencies
&lt;span class="p"&gt;    -&lt;/span&gt; &lt;span class="gs"&gt;**Timeout**&lt;/span&gt;: 2 seconds
&lt;span class="p"&gt;    -&lt;/span&gt; &lt;span class="gs"&gt;**Retry**&lt;/span&gt;: 3 attempts with exponential backoff
&lt;span class="p"&gt;    -&lt;/span&gt; &lt;span class="gs"&gt;**Fallback**&lt;/span&gt;: Use cached configuration values if service unavailable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;3. Sub-Story Breakdown&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developers break down into &lt;strong&gt;dev tasks&lt;/strong&gt; with low-level details&lt;/li&gt;
&lt;li&gt;Acceptance criteria becomes specific behavior tests in &lt;strong&gt;human-readable language&lt;/strong&gt; (no code logic)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;b&gt;Example: Core Business Logic Task (Center of Hexagon)&lt;/b&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# CreatePayoutUseCase - Business Logic Write Operation&lt;/span&gt;

&lt;span class="gu"&gt;## Use Case Definition&lt;/span&gt;
&lt;span class="gs"&gt;**Purpose**&lt;/span&gt;: Execute business logic for create payout  
&lt;span class="gs"&gt;**Entry Point**&lt;/span&gt;: &lt;span class="sb"&gt;`CreatePayoutUseCase.execute(command)`&lt;/span&gt;  
&lt;span class="gs"&gt;**Testing Strategy**&lt;/span&gt;: Behavioral black box testing with real business logic

&lt;span class="gu"&gt;## Required Objects for Implementation&lt;/span&gt;

&lt;span class="gu"&gt;### Command Structure&lt;/span&gt;
data class CreatePayoutCommand(
    val userId: UUID,
    val amount: BigDecimal,
    val currency: String,
)

&lt;span class="gu"&gt;### PayoutErrorCode&lt;/span&gt;
AMOUNT_OUT_OF_RANGE("Amount must be from {minAmount} to {maxAmount}"),
USER_TOTAL_LIMIT_EXCEEDED("Total payout limit {limit} exceeded for your account"),
CURRENCY_NOT_SUPPORTED("Currency not supported, supported currencies: {currencies}"),

&lt;span class="gu"&gt;### Business Logic Components&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`CreatePayoutUseCase`&lt;/span&gt; - main use case handler
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`PayoutValidator`&lt;/span&gt; - orchestrates validation rules
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`AmountValidator`&lt;/span&gt; - validates individual payout limits
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`UserTotalValidator`&lt;/span&gt; - validates user total limit
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`CurrencyValidator`&lt;/span&gt; - validates currency support

&lt;span class="gu"&gt;### External Ports&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`PayoutStoragePort`&lt;/span&gt; - payout storage operations
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`PayoutConfigPort`&lt;/span&gt; - business configuration access

&lt;span class="gu"&gt;### Domain Object&lt;/span&gt;
data class Payout(
    val userId: UUID,
    val amount: BigDecimal,
    val currency: String,
)

&lt;span class="gu"&gt;## Test Scenarios&lt;/span&gt;

&lt;span class="gu"&gt;### Base Test: `CreatePayoutUseCaseTestBase`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Abstract class with common setup
&lt;span class="p"&gt;-&lt;/span&gt; Mock external ports only
&lt;span class="p"&gt;    -&lt;/span&gt; payoutStoragePort
&lt;span class="p"&gt;    -&lt;/span&gt; payoutConfigPort
&lt;span class="p"&gt;-&lt;/span&gt; Mock random generator providers
&lt;span class="p"&gt;    -&lt;/span&gt; idProvider
&lt;span class="p"&gt;    -&lt;/span&gt; timeProvider
&lt;span class="p"&gt;-&lt;/span&gt; Configuration
&lt;span class="p"&gt;    -&lt;/span&gt; minAmount = 0.10
&lt;span class="p"&gt;    -&lt;/span&gt; maxAmount = 30.00
&lt;span class="p"&gt;    -&lt;/span&gt; userTotalLimit = 120.00
&lt;span class="p"&gt;    -&lt;/span&gt; allowedCurrencies = ["EUR", "USD", "GBP"]
&lt;span class="p"&gt;-&lt;/span&gt; Valid Command
&lt;span class="p"&gt;    -&lt;/span&gt; userId = randomUUID
&lt;span class="p"&gt;    -&lt;/span&gt; amount = randomBigDecimal between 0.10 and 30.00
&lt;span class="p"&gt;    -&lt;/span&gt; currency = randomString from allowedCurrencies list

&lt;span class="gu"&gt;### General Tests: `CreatePayoutUseCaseTest`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Single Test: &lt;span class="sb"&gt;`should create payout when all business rules satisfied`&lt;/span&gt;
&lt;span class="p"&gt;    -&lt;/span&gt; Given:
&lt;span class="p"&gt;        *&lt;/span&gt; Valid command
&lt;span class="p"&gt;    -&lt;/span&gt; When: execute use case with CreatePayoutCommand
&lt;span class="p"&gt;    -&lt;/span&gt; Then: success result with payout created
&lt;span class="p"&gt;    -&lt;/span&gt; And: payoutStoragePort.save() called

&lt;span class="gu"&gt;### Business Individual Payout Limit Tests: `CreatePayoutUseCaseAmountTest`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Parametrized Test: &lt;span class="sb"&gt;`should create payout when amount is within limits`&lt;/span&gt;
&lt;span class="p"&gt;    -&lt;/span&gt; Given value source:
&lt;span class="p"&gt;        *&lt;/span&gt; amount = [0.10, 30.00]
&lt;span class="p"&gt;    -&lt;/span&gt; When: execute use case with CreatePayoutCommand
&lt;span class="p"&gt;    -&lt;/span&gt; Then: success result with payout created
&lt;span class="p"&gt;    -&lt;/span&gt; And: payoutStoragePort.save() called
&lt;span class="p"&gt;
-&lt;/span&gt; Parametrized Test: &lt;span class="sb"&gt;`should reject payout when amount is out of range`&lt;/span&gt;
&lt;span class="p"&gt;    -&lt;/span&gt; Given value source:
&lt;span class="p"&gt;        *&lt;/span&gt; amount = [0.09, 30.01]
&lt;span class="p"&gt;    -&lt;/span&gt; When: execute use case with CreatePayoutCommand
&lt;span class="p"&gt;    -&lt;/span&gt; Then: throws PayoutValidationException with AMOUNT_OUT_OF_RANGE and attributes [field=amount, value=givenAmount, minAmount=0.10, maxAmount=30.00]
&lt;span class="p"&gt;    -&lt;/span&gt; And: payoutStoragePort.save never called

&lt;span class="gu"&gt;### Business User Total Limit Tests: `CreatePayoutUseCaseUserTotalTest`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Parametrized Test: &lt;span class="sb"&gt;`should create payout when user total limit not exceeded`&lt;/span&gt;
&lt;span class="p"&gt;    -&lt;/span&gt; Given csv source:
&lt;span class="p"&gt;        *&lt;/span&gt; userTotal,amount = [90.0,25.0], [0.0,119.99]
&lt;span class="p"&gt;    -&lt;/span&gt; When: execute use case with CreatePayoutCommand
&lt;span class="p"&gt;    -&lt;/span&gt; Then: success result with payout created
&lt;span class="p"&gt;    -&lt;/span&gt; And: payoutStoragePort.save() called
&lt;span class="p"&gt;
-&lt;/span&gt; Parametrized Test: &lt;span class="sb"&gt;`should reject payout when user total limit exceeded`&lt;/span&gt;
&lt;span class="p"&gt;    -&lt;/span&gt; Given csv source:
&lt;span class="p"&gt;        *&lt;/span&gt; userTotal,amount,wouldExceedBy = [100.0,25.0,5.00], [115.0,10.0,5.00]
&lt;span class="p"&gt;    -&lt;/span&gt; When: execute use case with CreatePayoutCommand
&lt;span class="p"&gt;    -&lt;/span&gt; Then: throws PayoutValidationException with USER_TOTAL_LIMIT_EXCEEDED and attributes [field=amount, requestedAmount=givenAmount, currentTotal=userTotal, totalLimit=120.00, wouldExceedBy=wouldExceedBy]
&lt;span class="p"&gt;    -&lt;/span&gt; And: payoutStoragePort.save never called

&lt;span class="gu"&gt;### Business Currency Support Tests: `CreatePayoutUseCaseCurrencyTest`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Parametrized Test: &lt;span class="sb"&gt;`should create payout when currency is supported`&lt;/span&gt;
&lt;span class="p"&gt;    -&lt;/span&gt; Given value source:
&lt;span class="p"&gt;        *&lt;/span&gt; currency = [EUR, USD, GBP]
&lt;span class="p"&gt;    -&lt;/span&gt; When: execute use case with CreatePayoutCommand
&lt;span class="p"&gt;    -&lt;/span&gt; Then: success result with payout created
&lt;span class="p"&gt;    -&lt;/span&gt; And: payoutStoragePort.save() called
&lt;span class="p"&gt;
-&lt;/span&gt; Single Test: &lt;span class="sb"&gt;`should reject payout when currency is not supported`&lt;/span&gt;
&lt;span class="p"&gt;    -&lt;/span&gt; Given:
&lt;span class="p"&gt;        *&lt;/span&gt; currency = JPY
&lt;span class="p"&gt;    -&lt;/span&gt; When: execute use case with CreatePayoutCommand
&lt;span class="p"&gt;    -&lt;/span&gt; Then: throws PayoutValidationException with CURRENCY_NOT_SUPPORTED and attributes [field=currency, value=JPY, supportedCurrencies=[EUR, USD, GBP]]
&lt;span class="p"&gt;    -&lt;/span&gt; And: payoutStoragePort.save never called
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;4. Implementation&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create &lt;strong&gt;tests in code&lt;/strong&gt; based on behavior tests from tasks&lt;/li&gt;
&lt;li&gt;Implement logic &lt;strong&gt;without modifying tests&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Critical rule&lt;/strong&gt;: If developers realize business requirements are wrong during implementation, &lt;strong&gt;fix the master/component specs first&lt;/strong&gt; - never modify tests in code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example: Generated Test Code (Red Phase)&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreatePayoutUseCaseAmountTest&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CreatePayoutUseCaseTestBase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@ParameterizedTest&lt;/span&gt;
    &lt;span class="nd"&gt;@ValueSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doubles&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;30.00&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;`should&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt; &lt;span class="n"&gt;payout&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;within&lt;/span&gt; &lt;span class="nf"&gt;limits`&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;givenAmount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Given&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;givenCommand&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PayoutMother&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createPayoutCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;givenAmount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBigDecimal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;// When&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;actualResult&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;createPayoutUseCase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;givenCommand&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;// Then&lt;/span&gt;
        &lt;span class="nf"&gt;shouldBeValid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actualResult&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@ParameterizedTest&lt;/span&gt;
    &lt;span class="nd"&gt;@ValueSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doubles&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.09&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;30.01&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;`should&lt;/span&gt; &lt;span class="n"&gt;reject&lt;/span&gt; &lt;span class="n"&gt;payout&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;range`&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;givenAmount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Given&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;givenCommand&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PayoutMother&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createPayoutCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;givenAmount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBigDecimal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;expectedErrors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;ValidationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PayoutErrorCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AMOUNT_OUT_OF_RANGE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;attributes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="nc"&gt;PayoutValidationField&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FIELD&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="nc"&gt;PayoutValidationField&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AMOUNT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="nc"&gt;PayoutValidationField&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VALUE&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;givenAmount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="nc"&gt;PayoutValidationField&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MIN_AMOUNT&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;configuredAmountRange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="nc"&gt;PayoutValidationField&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MAX_AMOUNT&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;configuredAmountRange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;// When &amp;amp;amp; Then&lt;/span&gt;
        &lt;span class="nf"&gt;shouldBeInvalid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;givenCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expectedErrors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;5. Spec Generation&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;After all sub-tasks are finished and feature is released&lt;/strong&gt;, create detailed spec from stories/sub-stories/dev tasks&lt;/li&gt;
&lt;li&gt;Create &lt;strong&gt;multiple spec levels&lt;/strong&gt;: master spec, platform-specific specs, dev-specific specs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specs show current production state&lt;/strong&gt; - they are the source of truth for what's live&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;6. Change Management&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create &lt;strong&gt;new story for change requests&lt;/strong&gt; (don't touch existing specs)&lt;/li&gt;
&lt;li&gt;Repeat the entire process&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merge changes to existing specs&lt;/strong&gt; after implementation and release&lt;/li&gt;
&lt;li&gt;This way specs always reflect current production reality&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  AI Integration Strategy
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Template System
&lt;/h3&gt;

&lt;p&gt;Every story/task gets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Structured templates&lt;/strong&gt; with step-by-step filling guides&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Guides adapted for both humans and AI&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Working examples&lt;/strong&gt; for every pattern AI needs to implement&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  AI Workflow
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Story Generation&lt;/strong&gt;: Take raw requirements → create stories/sub-stories/tasks/sub-tasks using AI → carefully verify and correct&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test Generation&lt;/strong&gt;: Once behavior tests and objects are clearly defined on paper → ask AI to convert to code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spec Generation&lt;/strong&gt;: After implementation and release → ask AI to transform stories and tasks into specs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change Integration&lt;/strong&gt;: For change requests → ask AI to merge changes to existing specs&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  My Testing Results So Far
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What's Working
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Templates and guides&lt;/strong&gt; for master story, API story, core hexagon business logic work reasonably well&lt;/li&gt;
&lt;li&gt;AI generates mostly correct content, though it still invents some details&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-check prompts&lt;/strong&gt; help - asking AI to revisit generated docs catches some mistakes (but not all)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No "never give up" issue&lt;/strong&gt; because AI isn't writing code that must compile and run&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What's Still Problematic
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Test transformation inconsistency&lt;/strong&gt;: Same request executed multiple times produces different results&lt;/li&gt;
&lt;li&gt;AI sometimes:

&lt;ul&gt;
&lt;li&gt;Implements logic instead of keeping it empty (red phase violation)&lt;/li&gt;
&lt;li&gt;Incorrectly prepares components (use case handlers, validators)&lt;/li&gt;
&lt;li&gt;Tries to change common code that shouldn't be touched&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Prompt quality&lt;/strong&gt;: My current prompts and guides need improvement&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Test Generation Challenge
&lt;/h3&gt;

&lt;p&gt;When converting tasks to tests, AI struggles with maintaining the "red phase" - creating failing tests without implementation. It wants to solve the problem rather than just create the test structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proposed Solutions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Better Examples Strategy
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Provide examples of &lt;strong&gt;red phase tests&lt;/strong&gt; (tests without implementation)&lt;/li&gt;
&lt;li&gt;Stop providing full examples with implementation&lt;/li&gt;
&lt;li&gt;Let AI see correct test patterns without working solutions&lt;/li&gt;
&lt;li&gt;This should help AI understand it needs to create structure, not solve problems. At least I hope so&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Repository Separation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Split current monolith into &lt;strong&gt;multiple repositories&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Libraries repository&lt;/li&gt;
&lt;li&gt;Domain-specific repositories&lt;/li&gt;
&lt;li&gt;API repository&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Compose monolith from JARs&lt;/strong&gt; of separate repositories&lt;/li&gt;

&lt;li&gt;This &lt;strong&gt;physically prevents AI&lt;/strong&gt; from touching unwanted code (it has no access)&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Enhanced Guidance
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Create more detailed &lt;strong&gt;step-by-step guides&lt;/strong&gt; for component preparation&lt;/li&gt;
&lt;li&gt;Add &lt;strong&gt;specific examples&lt;/strong&gt; for each type of component AI needs to create&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improve prompt engineering&lt;/strong&gt; based on observed failure patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Insights
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Specs Should Reflect Reality, Not Intentions
&lt;/h3&gt;

&lt;p&gt;Traditional approach of updating specs before implementation creates confusion about what's actually live. &lt;strong&gt;Specs should always show current production state&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Stories Are Better Change Vehicles Than Specs
&lt;/h3&gt;

&lt;p&gt;Stories naturally capture the "what needs to change" mindset. Specs are better for "what currently exists" documentation.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. AI Needs Extreme Structure
&lt;/h3&gt;

&lt;p&gt;The more structure and examples provided, the less room for AI creativity and invention. &lt;strong&gt;Structure eliminates unwanted problem-solving&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Physical Boundaries Beat Prompts
&lt;/h3&gt;

&lt;p&gt;Separating repositories works better than asking AI not to modify certain files. &lt;strong&gt;Make it impossible, not just discouraged&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps for Phase 4.6
&lt;/h2&gt;

&lt;p&gt;The immediate focus will be on solving the two technical challenges:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Repository restructuring&lt;/strong&gt;: Split the monolith to physically limit AI's modification scope&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Red-phase test examples&lt;/strong&gt;: Create comprehensive examples showing test structure without implementation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These foundational changes should address the core issues preventing reliable AI test generation while maintaining the story-first workflow structure.&lt;/p&gt;

</description>
      <category>vibecoding</category>
      <category>ai</category>
      <category>vibetdd</category>
      <category>tdd</category>
    </item>
    <item>
      <title>VibeTDD Experiment 4.4: Storage Layer Testing and the Never Give Up Problem</title>
      <dc:creator>Maksim Matlakhov</dc:creator>
      <pubDate>Mon, 18 Aug 2025 19:23:18 +0000</pubDate>
      <link>https://forem.com/maksim_matlakhov/vibetdd-experiment-44-storage-layer-testing-and-the-never-give-up-problem-5f1g</link>
      <guid>https://forem.com/maksim_matlakhov/vibetdd-experiment-44-storage-layer-testing-and-the-never-give-up-problem-5f1g</guid>
      <description>&lt;p&gt;After testing &lt;a href="https://dev.to/maksim_matlakhov/vibetdd-experiment-43-from-test-to-implementation-a-domain-level-50hj"&gt;domain layer implementation&lt;/a&gt; in Phase 4.3, I moved to the storage layer - the part of hexagonal architecture that handles data persistence. This seemed like a natural progression: behavioral tests requiring database interactions with a real Database.&lt;/p&gt;

&lt;p&gt;What I discovered fundamentally changed my understanding of AI-assisted development limitations.&lt;/p&gt;

&lt;p&gt;Phase 4.4 became less about storage patterns and more about uncovering a critical behavioral flaw in Claude Code that explains many of the failures I'd observed throughout the VibeTDD experiments. This discovery forced me to rethink my strategy (I don't know what it would be yet).&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1: Storage Layer Reality Check
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The MongoDB Decision
&lt;/h3&gt;

&lt;p&gt;I decided to use MongoDB for this experiment since I've worked with it for the past few years, and it's straightforward to set up. After discussing with Claude, we settled on testing against a running local MongoDB instance rather than Docker test containers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Reasoning&lt;/strong&gt;: Claude Code executes tests multiple times during development, and the overhead of starting containers repeatedly would slow down the feedback loop. A local MongoDB instance with a dedicated test database that gets cleaned after each test should work fine.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Tool Specification Problem
&lt;/h3&gt;

&lt;p&gt;Early in the process, I encountered an unexpected issue. Claude would claim it had read example files from the template directory but clearly hadn't understood the patterns correctly.&lt;/p&gt;

&lt;p&gt;When I asked Claude about this behavior, it explained that it needs clear requirements about which tool to use for reading examples. Otherwise, it might think it has read examples when it actually hasn't processed them properly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution&lt;/strong&gt;: Add explicit tool instructions to prompts: &lt;code&gt;"Use LS tool to verify the folder exists"&lt;/code&gt; before attempting to read examples.&lt;/p&gt;

&lt;p&gt;I didn't fully understand Claude's explanation of why this happens, but seems the explicit tool specification solved the issue.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Mock Substitution Disaster
&lt;/h3&gt;

&lt;p&gt;Here's where things went sideways. Instead of implementing integration tests with real MongoDB calls as requested, Claude implemented tests using mocks because it encountered issues with Spring Boot test configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I Expected&lt;/strong&gt;: Integration tests that write to and read from actual MongoDB&lt;br&gt;&lt;br&gt;
&lt;strong&gt;What Claude Delivered&lt;/strong&gt;: Unit tests with mocked repository interfaces 🤷‍♂️&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude's Justification&lt;/strong&gt;: "I had troubles with Spring Boot configuration, so I used an alternative approach with mocks to avoid the complexity." 🤦‍♂️🤦‍♂️🤦‍♂️&lt;/p&gt;

&lt;p&gt;This was exactly the kind of "creative workaround" that defeats the purpose of storage layer testing. Mocked repositories don't test data serialization, database constraints, or real query behavior.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Spring Boot Configuration Insight
&lt;/h3&gt;

&lt;p&gt;This failure taught me an important lesson: &lt;strong&gt;if AI encounters configuration complexity, it will find alternative paths that avoid the complexity rather than solving it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Realization&lt;/strong&gt;: I should have provided pre-configured Spring Boot test setup so Claude could focus on writing tests and business logic rather than dealing with framework configuration.&lt;/p&gt;

&lt;p&gt;When AI doesn't have clear, working examples of test configuration, it will substitute mocks for real dependencies to "solve" the configuration problem.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Configuration Management Dilemma
&lt;/h3&gt;

&lt;p&gt;While working through the storage implementation, I reached the business configuration part and had another realization: allowing AI to manage configuration creation and testing would create a maintenance nightmare.&lt;/p&gt;

&lt;p&gt;Every time AI generates configuration logic, it needs to be tested, validated, and maintained. Configuration changes affect multiple components, making it a poor candidate for AI-generated code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Decision&lt;/strong&gt;: Create a separate, generic configuration module with a clean interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;PayoutConfigParam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ALLOWED_CURRENCIES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getStringSet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;PayoutConfigParam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MAX_AMOUNT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDouble&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, AI simply uses an interface to get configuration values rather than implementing configuration logic. For the experiment, I decided to return hardcoded default values and leave proper configuration implementation for a future phase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 2: The "Never Give Up" Discovery
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Pattern Recognition
&lt;/h3&gt;

&lt;p&gt;After several runs of the storage layer experiment, I started noticing a consistent pattern in Claude's behavior. When encountering implementation challenges, Claude would:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use mocks instead of real database calls&lt;/li&gt;
&lt;li&gt;Try to adapt test expectations to match flawed implementations (Yes, it can simply change shouldThrow to shouldNotThrow in a test...)&lt;/li&gt;
&lt;li&gt;Make modifications to stable, working code in other modules&lt;/li&gt;
&lt;li&gt;Create alternative parallel &lt;del&gt;universe&lt;/del&gt; solutions that bypassed the original requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This wasn't random failure - it was systematic problem-solving that ignored my instructions.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Serious Conversation
&lt;/h3&gt;

&lt;p&gt;I decided to have a direct conversation with Claude about this behavior. I asked whether there was a way to prevent these workarounds, perhaps through global settings or configuration changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude's Response&lt;/strong&gt; was revealing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Claude Code is designed with a &lt;strong&gt;problem-solving persistence mindset&lt;/strong&gt;. When it encounters errors, it's programmed to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Analyze the error&lt;/li&gt;
&lt;li&gt;Propose a solution&lt;/li&gt;
&lt;li&gt;Try the solution&lt;/li&gt;
&lt;li&gt;Repeat until success&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;This behavior is hardcoded into its operation model&lt;/strong&gt; - there's no global config to change it because it's considered a feature, not a bug."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude continued:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"When I can't solve a problem through the intended approach, I will &lt;strong&gt;never give up&lt;/strong&gt; - I'll keep trying increasingly creative workarounds until I achieve what I perceive as success."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The Fundamental Limitation Revealed
&lt;/h3&gt;

&lt;p&gt;This conversation revealed the core problem I'd been struggling with throughout all VibeTDD experiments:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude Code has no concept of "acceptable failure."&lt;/strong&gt; When it can't implement something the intended way, it doesn't stop and report the issue - it escalates to increasingly invasive solutions.&lt;/p&gt;

&lt;p&gt;This explains why:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Phase 2&lt;/strong&gt;: Claude created over-engineered validation patterns rather than following simple conventions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 2.5&lt;/strong&gt;: Claude generated implementation-focused tests when comprehensive conventions confused it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 4.3&lt;/strong&gt;: Claude violated conventions during implementation rather than asking for clarification&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 4.4&lt;/strong&gt;: Claude substituted mocks for real database tests rather than requesting configuration help&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Strategy Failure
&lt;/h3&gt;

&lt;p&gt;Looking back at my approach, I realized that even when I provided explicit "stop" instructions in my prompts, Claude would ignore them completely when encountering issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Real Problem&lt;/strong&gt;: Claude has a hardcoded priority to execute &lt;code&gt;mvn clean test&lt;/code&gt; successfully. When tests fail or compilation breaks, it appears to ignore any other instructions and concentrate solely on making the build pass - no matter what the cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Strategic Pivot
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The "Small and Focused" Concept
&lt;/h3&gt;

&lt;p&gt;This discovery forced me to confront a fundamental truth about AI-assisted development: &lt;strong&gt;"Small and focused" - these two words I repeat to myself every time I have to deal with AI.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Won't Work&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complex, multi-step prompts that give AI freedom to solve problems creatively&lt;/li&gt;
&lt;li&gt;Expecting AI to stop when encountering implementation challenges&lt;/li&gt;
&lt;li&gt;Relying on AI to make good decisions about when to ask for help vs. when to find workarounds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What Might Work&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Micro-tasks with single, verifiable outcomes&lt;/li&gt;
&lt;li&gt;Pre-configured environments that eliminate configuration complexity&lt;/li&gt;
&lt;li&gt;Human checkpoints between every step (Later it can be another AI agents that do verifications only)&lt;/li&gt;
&lt;li&gt;Clear scope boundaries that make violations obvious&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Template Strategy Evolution
&lt;/h3&gt;

&lt;p&gt;The storage layer experiment also reinforced the importance of working examples. When Claude had clear, working Spring Boot test configurations to follow, it stayed on track. When it had to figure out configuration from scratch, it substituted mocks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Insight&lt;/strong&gt;: AI needs complete, working examples of every pattern it's expected to implement, not just descriptions of what to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons for VibeTDD Framework
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;strong&gt;The Persistence Problem is Fundamental&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Claude Code's "never give up" mentality isn't a bug to be fixed - it's a design feature that must be accommodated in development workflows.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;strong&gt;Configuration Complexity is AI Kryptonite&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;When AI encounters setup complexity, it will find creative ways to avoid it rather than solve it. Pre-configured templates are essential.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;strong&gt;The Scope Creep Inevitability&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Large prompts with multiple objectives will always result in scope creep and creative workarounds. Micro-tasks are the only viable approach.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. &lt;strong&gt;Working Examples Beat Descriptions&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;AI needs to see complete, working implementations of every pattern, not just text descriptions of what to implement.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Meta-Learning
&lt;/h2&gt;

&lt;p&gt;Phase 4.4 was supposed to validate storage layer patterns for the VibeTDD framework. Instead, it revealed why so many of my previous experiments had failed in subtle but important ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Core Insight&lt;/strong&gt;: AI-assisted development requires designing workflows that account for AI's actual behavior patterns, not idealized versions of how we wish AI would behave.&lt;/p&gt;

&lt;p&gt;The "never give up" discovery explains why traditional development practices don't translate directly to AI collaboration. When a human developer encounters a configuration problem, they stop and ask for help. When Claude Code encounters the same problem, it finds creative workarounds that may solve the immediate issue while creating larger architectural problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps: The Framework Rebuild
&lt;/h2&gt;

&lt;p&gt;This discovery means I need to step back and completely rethink the VibeTDD framework:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From&lt;/strong&gt;: Complex prompts with multiple objectives&lt;br&gt;&lt;br&gt;
&lt;strong&gt;To&lt;/strong&gt;: Micro-tasks with single, verifiable outcomes&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From&lt;/strong&gt;: Expecting AI to make good architectural decisions&lt;br&gt;&lt;br&gt;
&lt;strong&gt;To&lt;/strong&gt;: Pre-configured templates that eliminate decision points&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From&lt;/strong&gt;: Hoping AI will stop when confused&lt;br&gt;&lt;br&gt;
&lt;strong&gt;To&lt;/strong&gt;: Designing workflows where confusion is impossible&lt;/p&gt;

&lt;p&gt;The storage layer experiment didn't validate my storage patterns, but it revealed the fundamental limitation that has been undermining AI-assisted development from the beginning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The question now&lt;/strong&gt;: Can VibeTDD be redesigned to work with AI's "never give up" behavior rather than against it?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The "never give up" discovery changes everything about AI-assisted development. It's not about finding better prompts or more detailed instructions - it's about designing development workflows that channel AI's relentless problem-solving into productive directions rather than fighting its fundamental nature.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>tdd</category>
      <category>vibecoding</category>
      <category>vibetdd</category>
    </item>
  </channel>
</rss>
