<?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>DEV Community: Martin Simon</title>
    <description>The latest articles on DEV Community by Martin Simon (@thesimdak).</description>
    <link>https://dev.to/thesimdak</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%2F400850%2F13586e2b-e401-429b-b8cf-b21cca5b364c.png</url>
      <title>DEV Community: Martin Simon</title>
      <link>https://dev.to/thesimdak</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thesimdak"/>
    <language>en</language>
    <item>
      <title>Shared library for Micro-Services: Why should you have one?</title>
      <dc:creator>Martin Simon</dc:creator>
      <pubDate>Tue, 29 Apr 2025 17:34:49 +0000</pubDate>
      <link>https://dev.to/thesimdak/shared-library-for-micro-services-why-should-you-have-one-2jod</link>
      <guid>https://dev.to/thesimdak/shared-library-for-micro-services-why-should-you-have-one-2jod</guid>
      <description>&lt;p&gt;When I talk to developers about microservices, one thing always stands out: everyone seems to define microservices slightly differently. From how small a service should be to how tightly they interact, the implementation varies widely. But one topic comes up frequently and often sparks debate — shared libraries. Should microservices share code, or does that undermine their independence?&lt;/p&gt;

&lt;p&gt;Based on my experience, I believe that &lt;strong&gt;having a shared library can be a good idea — if you do it right&lt;/strong&gt;. In this post, I’ll explore why shared libraries can be useful, where the risks are, and how to approach them responsibly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Do We Mean by a Shared Library?
&lt;/h2&gt;

&lt;p&gt;A shared library is reusable code that multiple services can include as a dependency, typically managed using tools like Gradle or Maven in the Java ecosystem. These libraries usually provide utilities, technical abstractions, or common infrastructure integrations.&lt;/p&gt;

&lt;p&gt;Yes, microservices promise language and framework freedom. But in small to medium-sized companies, that flexibility often becomes a burden. It's more efficient to standardize around a single tech stack — and in that environment, sharing well-abstracted code can offer real productivity benefits.&lt;/p&gt;

&lt;p&gt;Think of frameworks like Spring Boot — we don’t write everything from scratch. In the same spirit, a shared library is your internal toolbox, tuned to your company’s needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Would You Want a Shared Library?
&lt;/h2&gt;

&lt;p&gt;Here are some practical reasons shared libraries can be valuable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Boost Productivity:&lt;/strong&gt; Avoid re-implementing the same boilerplate in every service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standardize Best Practices:&lt;/strong&gt; Enforce consistent logging, error handling, metrics, and more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encapsulate Technical Complexity:&lt;/strong&gt; Implement things like multi-tenancy, authentication, or service-to-service communication once and reuse them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some examples from real-world projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multi-tenant HTTP middleware&lt;/li&gt;
&lt;li&gt;Shared Spring Security configuration&lt;/li&gt;
&lt;li&gt;Multi-tenant MongoDB setup&lt;/li&gt;
&lt;li&gt;Kafka message header propagators&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren’t business rules — they’re technical concerns best solved once and reused.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where’s the Catch?
&lt;/h2&gt;

&lt;p&gt;Of course, shared libraries are not a silver bullet. They come with trade-offs that you should handle with care:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Avoid Business Logic
&lt;/h3&gt;

&lt;p&gt;Never include business rules or data models in shared libraries. That creates tight coupling between services and violates microservice independence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bad examples:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shared domain entities (JPA/Hibernate)&lt;/li&gt;
&lt;li&gt;Shared data access layers&lt;/li&gt;
&lt;li&gt;Shared decision logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Services should own their own business logic and evolve independently.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Versioning Matters
&lt;/h3&gt;

&lt;p&gt;Shared libraries should be versioned like any external dependency. Use semantic versioning, and assume services may not upgrade right away. This forces you to maintain backward compatibility and minimize breaking changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Ownership and Maintenance
&lt;/h3&gt;

&lt;p&gt;Someone needs to own the library. Without clear responsibility, it can become outdated, bloated, or inconsistent. Ideally, a platform or infra team maintains it — or at least one team with a clear mandate and review process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combine Shared Library and the BOM
&lt;/h2&gt;

&lt;p&gt;When using a shared library in your project alongside other external dependencies, managing dependency versions can become tricky. Inconsistent versions across microservices can lead to compatibility issues, especially as your codebase grows. Fortunately, frameworks like Spring Boot address this problem by providing a BOM (Bill of Materials), which predefines the version of each used dependency. This ensures that all dependencies are aligned and eliminates the risk of version mismatches.&lt;/p&gt;

&lt;p&gt;It’s a good idea to apply the same concept to your shared library. By using a BOM for your internal dependencies, you can maintain consistent versions across all services that rely on it. When using Spring Boot, you can even inherit from its BOM and simply add your own dependencies as needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simplified upgrades
&lt;/h3&gt;

&lt;p&gt;One of the biggest advantages of using a BOM is the simplicity of upgrading project dependencies. In a system with multiple microservices, maintaining dependency versions independently in each service can be a significant burden. With a BOM, you can centralize version management, making upgrades much more straightforward. Instead of updating dependencies in each individual service, you only need to upgrade the version of your shared library in the BOM.&lt;/p&gt;

&lt;p&gt;This means that when you need to update your shared library or any of its dependencies, you can apply that change across all services in a single step, rather than updating each service manually. This approach significantly reduces maintenance overhead and ensures that all services stay in sync with the latest versions of the shared libraries and dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommendations
&lt;/h2&gt;

&lt;p&gt;✅ Use a BOM for version management&lt;/p&gt;

&lt;p&gt;✅ Include technical utilities only (auth, logging, retries, etc.)&lt;/p&gt;

&lt;p&gt;✅ Use semantic versioning and release notes&lt;/p&gt;

&lt;p&gt;✅ Assign clear ownership and establish a contribution process&lt;/p&gt;

&lt;p&gt;❌ Don’t share business logic or domain models&lt;/p&gt;

&lt;p&gt;❌ Don’t force synchronous coupling through shared code&lt;/p&gt;

&lt;h3&gt;
  
  
  Treat Shared Libraries Like Third-Party Libraries
&lt;/h3&gt;

&lt;p&gt;This is the best mindset. A shared library should feel like an external dependency. You use it because it solves a general technical problem well — not because someone dropped a bunch of code into a common repo.&lt;/p&gt;

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

&lt;p&gt;Microservices are about independent, scalable, and loosely coupled components. But independence doesn’t mean isolation. Technical reuse — if done carefully — can boost productivity and consistency without violating the microservice philosophy.&lt;/p&gt;

&lt;p&gt;Just remember: share the tools, not the rules.&lt;/p&gt;

</description>
      <category>microservices</category>
      <category>architecture</category>
      <category>java</category>
      <category>springboot</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Martin Simon</dc:creator>
      <pubDate>Thu, 13 Mar 2025 12:57:36 +0000</pubDate>
      <link>https://dev.to/thesimdak/-3704</link>
      <guid>https://dev.to/thesimdak/-3704</guid>
      <description>&lt;div class="ltag__link"&gt;
  &lt;a href="/thesimdak" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&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%2Fuser%2Fprofile_image%2F400850%2F13586e2b-e401-429b-b8cf-b21cca5b364c.png" alt="thesimdak"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://dev.to/thesimdak/ditching-the-overhead-my-experience-with-go-htmx-tailwind-alpinejs-1gif" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Ditching the Overhead: My Experience with Go, HTMX, Tailwind &amp;amp; Alpine.js&lt;/h2&gt;
      &lt;h3&gt;Martin Simon ・ Mar 12&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


</description>
      <category>go</category>
      <category>htmx</category>
      <category>tailwindcss</category>
    </item>
    <item>
      <title>Ditching the Overhead: My Experience with Go, HTMX, Tailwind &amp; Alpine.js</title>
      <dc:creator>Martin Simon</dc:creator>
      <pubDate>Wed, 12 Mar 2025 18:59:14 +0000</pubDate>
      <link>https://dev.to/thesimdak/ditching-the-overhead-my-experience-with-go-htmx-tailwind-alpinejs-1gif</link>
      <guid>https://dev.to/thesimdak/ditching-the-overhead-my-experience-with-go-htmx-tailwind-alpinejs-1gif</guid>
      <description>&lt;h2&gt;
  
  
  Prologue
&lt;/h2&gt;

&lt;p&gt;As a software developer primarily focused on Java for backend systems, I occasionally enjoy developing simple web apps to stay up-to-date with frontend technologies. A few years ago, I developed a web app to display results from rope climbing competitions. To my surprise, it's still in use today. Back then, I had grand plans to enhance it with more features, but life had other plans, and the project gathered dust for several years.&lt;/p&gt;

&lt;p&gt;The app was built using Angular 9, and to streamline development, I used PrimeNG as the component library. When I recently decided to update it, I was taken aback by the challenges of upgrading to Angular 19. Some dependencies, including the component library, were no longer compatible.&lt;/p&gt;

&lt;p&gt;Honestly, this was frustrating. With a full-time job, family responsibilities, and other hobbies, I didn't want to spend my limited free time updating side projects every six months just to keep up with Angular's updates.&lt;/p&gt;

&lt;p&gt;So, I faced a choice: invest time in updating the project or explore something new and exciting. I chose the latter and decided to use a tech stack that has gained popularity recently—Go for the backend, and HTMX, Alpine.js, and Tailwind CSS for the frontend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I chose Go as Java developer
&lt;/h2&gt;

&lt;p&gt;Firstly, I love learning new things. I don't expect my side projects to make me rich; I see them as opportunities to have fun and learn something new. Additionally, I don't want to spend much on hosting, and since Java with Spring Boot has significant memory demands, I sought a lightweight alternative that I could host on my VPS with 1 GB RAM—including a database and Traefik for handling certificates.&lt;/p&gt;

&lt;p&gt;In this article, I want to motivate you to try out this stack. It brought me a lot of joy, and I'd like to pass on a few insights.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting goals
&lt;/h2&gt;

&lt;p&gt;My primary goal was to rewrite an existing web application from Angular + Spring Boot into Go + HTMX. Beyond that, I set several other objectives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backend Goals
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Learn Something New&lt;/strong&gt; -  I work with Java daily, so redoing this project in Java didn't make sense. I wanted a trending language with staying power. Go, introduced in 2007, has proven its market position.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KISS principle&lt;/strong&gt; - Make the application easy to maintain in the coming years.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low-memory footprint&lt;/strong&gt; - Keep hosting costs low.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Frontend Goals
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Avoid Dependency Hell&lt;/strong&gt; - Managing dependencies can be challenging, especially in the frontend ecosystem, where libraries frequently release new versions with less emphasis on backward compatibility.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find a Stable Stack&lt;/strong&gt; - I want to spend less time maintaining the project while keeping it up-to-date. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write Less JavaScript&lt;/strong&gt; - Reduce complexity by writing less JavaScript for my side projects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write Less CSS&lt;/strong&gt; - As a backend developer, I often feel overwhelmed by the amount of CSS in projects. I wanted to avoid the hassle of managing extensive CSS files.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Developing the Backend
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What I liked about Go
&lt;/h3&gt;

&lt;p&gt;Java has a stable and robust ecosystem, and it will take time for Go to reach a similar level. However, Go brings a refreshing perspective. I tried to use as few dependencies as possible to maximize performance and minimize overhead.&lt;/p&gt;

&lt;p&gt;I particularly liked the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Slqc&lt;/strong&gt; - A great library for generating code from plain SQL, focused on performance and utilization. I personally like this much more then ORM frameworks, where the control over generated SQL and its performance is only limited.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quick Startup&lt;/strong&gt; - Go apps start quickly compared to bloated Spring Boot applications, making hot deployments unnecessary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low Memory Footprint&lt;/strong&gt; - As someone who values fun over profit with side projects, keeping hosting costs low is crucial.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple Return Values&lt;/strong&gt; - A feature I've often missed in Java. While some might argue it goes against OOP principles, it saves a lot of code.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What I miss so far
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exception Handling&lt;/strong&gt; - Go's error handling is different and took some getting used to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stack Traces&lt;/strong&gt; - Java's stack traces are more informative.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Excelize&lt;/strong&gt; - A dependency for reading Excel files, similar to Apache POI. While easier to use than POI, it lacks some of POI's robustness and features.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Developing the Frontend
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Choosing the Frontend-Stack
&lt;/h2&gt;

&lt;p&gt;For my small projects, I value simplicity. Although I'm confident writing TypeScript in the Angular framework, I sought less overhead. I considered using plain JavaScript to avoid dependencies and breaking changes in Angular and related libraries. However, this would have introduced complexity elsewhere, requiring more code to handle basic tasks. I ended up with three minimal libraries that offer a relatively stable API with amazing additional value.&lt;/p&gt;

&lt;h2&gt;
  
  
  HTMX
&lt;/h2&gt;

&lt;p&gt;When I first read about HTMX, it immediately piqued my interest. This tiny library challenges the common pattern of splitting frontend and backend development into two independent applications, which has become the standard in recent years. It reminded me of the good old days of full-stack development with JSP.&lt;/p&gt;

&lt;p&gt;HTMX extends standard HTML with additional attributes to handle asynchronous calls to the backend, retrieving data to be displayed. Unlike JSON, HTMX works directly with the DOM, requiring the backend service to return the entire HTML, which is then automatically added to the DOM. Key features of HTMX include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AJAX Requests&lt;/strong&gt;: Easily fetch data from the server and update the page without a full reload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partial Page Updates&lt;/strong&gt;: Dynamically update specific parts of a webpage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event Handling&lt;/strong&gt;: Manage user interactions like clicks, hovers, and form submissions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With HTMX, I easily implemented:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Loading Spinner&lt;/strong&gt; - The hx-indicator attribute controls the display of a loading indicator with literally no additional code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pushing URL State&lt;/strong&gt; - When a partial page update should also update the URL state, the attribute "hx-push-url" does the job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partial Update of Pages&lt;/strong&gt; - Switching between menu items or triggering actions renders data in any defined area on the page.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Alpine.js
&lt;/h3&gt;

&lt;p&gt;One of my goals was to write as little JavaScript as possible. Alpine.js complements HTMX perfectly, adding reactivity to the frontend without writing a single line of JavaScript.&lt;/p&gt;

&lt;p&gt;With Alpine.js, I easily implemented:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Custom Dropdown Component&lt;/strong&gt; - Without a component library, I had to implement my own custom dropdown. Surprisingly, this was easy using a few attributes to control the state and handle events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confirmation pop-ups&lt;/strong&gt; - Showing or hiding elements on the page couldn't be easier with Alpine.js.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Menus with highlighted selected items&lt;/strong&gt; - Highlighting the currently selected menu item is a local state that must be managed within the application. Alpine.js excels at handling such tasks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tailwind CSS
&lt;/h3&gt;

&lt;p&gt;Tailwind CSS makes writing custom CSS obsolete. It offers predefined classes and an intuitive system that's easy to use. Plus, Tailwind is fully customizable, allowing you to extend and adapt styles and colors to your needs.&lt;/p&gt;

&lt;p&gt;Moreover, Tailwind's stability regarding backward compatibility is commendable. The framework's developers prioritize maintaining a stable API, ensuring that updates don't break existing styles. This stability is crucial for long-term projects, as it minimizes the time spent on maintenance and allows developers to focus on adding new features rather than fixing broken styles. With Tailwind, you can confidently build and scale your projects, knowing that your styling foundation is robust and future-proof.&lt;/p&gt;

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

&lt;p&gt;In conclusion, transitioning my web app from Angular and Spring Boot to Go, HTMX, Alpine.js, and Tailwind CSS was a rewarding experience that offered numerous benefits. This new stack not only enhanced my productivity but also provided a stable and efficient foundation for future development. Go's simplicity and performance, combined with the intuitive frontend solutions offered by HTMX and Alpine.js, made the development process enjoyable and less cumbersome. While there are challenges, such as the limited reusability of components in larger projects, the overall advantages of this stack make it a compelling choice for both personal and professional projects.&lt;/p&gt;

</description>
      <category>go</category>
      <category>alpinejs</category>
      <category>webdev</category>
      <category>htmx</category>
    </item>
    <item>
      <title>Multi-Tenancy in Java-Based Microservice-Platform</title>
      <dc:creator>Martin Simon</dc:creator>
      <pubDate>Wed, 05 Feb 2025 20:07:10 +0000</pubDate>
      <link>https://dev.to/thesimdak/multi-tenancy-in-java-based-microservice-platform-5e20</link>
      <guid>https://dev.to/thesimdak/multi-tenancy-in-java-based-microservice-platform-5e20</guid>
      <description>&lt;p&gt;Supporting multi-tenancy is often a crucial step for scaling your application and expanding its reach to a broader customer base. However, building this capability introduces a layer of complexity to your application, particularly when working within a microservice architecture. In this article, we’ll explore ways to manage this complexity effectively and discuss practical approaches to implementing multi-tenancy with ease.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is a Multi-Tenancy
&lt;/h2&gt;

&lt;p&gt;Multi-tenancy is the capability of a system or application to support multiple tenants (such as customers or organizations) within a single running instance while maintaining logical isolation. This approach optimizes resource utilization, reducing infrastructure and maintenance costs by sharing hardware, software, and operational overhead.&lt;/p&gt;

&lt;p&gt;A well-implemented multi-tenancy solution should be seamless for tenants—they shouldn’t notice that they are sharing the instance with others. Tenant isolation can be achieved at different levels, such as the database, authentication, or application code itself.&lt;/p&gt;

&lt;p&gt;Since multi-tenancy introduces additional complexity, the architectural decisions made during implementation significantly impact scalability, security, and future costs. In this article, we will explore how to handle tenants across different layers of an application. We will specifically focus on isolation at the application layer and briefly discuss approaches for data separation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing a Tenant Context
&lt;/h2&gt;

&lt;p&gt;When implementing tenant isolation at the application level, you will quickly realize that you need to have a tenant identifier available in different parts of your application code. We will refer to this identifier as &lt;code&gt;tenant-id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Passing this ID across multiple layers of an application can be cumbersome, adding unnecessary complexity and making code maintenance and testability significantly more challenging. If every interaction with the application logic requires the &lt;code&gt;tenant-id&lt;/code&gt;, we can introduce the concept of a tenant context, which centrally holds this information.&lt;/p&gt;

&lt;p&gt;Since Java is a multi-threaded language, we can leverage thread-local storage to manage the tenant context efficiently. This approach allows us to associate the tenant-id with the current thread, making it accessible throughout the application's execution flow. However, it's important to note that this method does not work in reactive frameworks, as they handle concurrency differently from traditional thread-based processing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using ThreadLocal to Keep the Context
&lt;/h2&gt;

&lt;p&gt;This simple class will be the core of our multi-tenancy implementation. It contains only three methods and very little logic, but it is powerful and provides incredible flexibility.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TenantContext&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ThreadLocal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;tenantIdHolder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ThreadLocal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;setTenantId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;tenantId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenantId&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TenantContextException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Tenant ID cannot be null"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;tenantIdHolder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenantId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="no"&gt;MDC&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tenantId"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenantId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;getTenantId&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;tenantId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tenantIdHolder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenantId&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TenantContextException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Tenant ID is not set in the current thread"&lt;/span&gt;&lt;span class="o"&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;tenantId&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;tenantIdHolder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;remove&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="no"&gt;MDC&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;remove&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tenantId"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; 
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whenever a request arrives at the application, we are expected to set the &lt;code&gt;TenantContext&lt;/code&gt; using the setter, which stores the &lt;code&gt;tenant-id&lt;/code&gt; in a &lt;code&gt;ThreadLocal&lt;/code&gt;. This ensures that whenever we call &lt;code&gt;getTenantId()&lt;/code&gt; within the same thread where the context was set, we reliably obtain the correct &lt;code&gt;tenant-id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;clear&lt;/code&gt; method is also very important. Since threads in Java can be reused (for example, in thread pools), we must clear the context once processing is complete to prevent unintended data leakage between requests. This cleanup is typically performed in an interceptor after a REST call or when Kafka message processing ends.&lt;/p&gt;

&lt;p&gt;Additionally, you can see that we put the tenant ID into the MDC, which allows us to clearly associate logs with a particular tenant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Passing a Tenant Context between Microservices
&lt;/h2&gt;

&lt;p&gt;In a microservice architecture, requests often travel across multiple services, and it's crucial to pass tenant information between them. Ensuring proper tenant context propagation is essential for maintaining data isolation and security.&lt;/p&gt;

&lt;p&gt;For simplicity, we'll focus on two of the most common communication methods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Synchronous calls via REST API&lt;/li&gt;
&lt;li&gt;Asynchronous calls via Kafka&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Header vs. Body: Where to Pass the Tenant Id?
&lt;/h3&gt;

&lt;p&gt;Regardless of the communication channel, one key decision is where to include the &lt;code&gt;tenant-id&lt;/code&gt; — in the header or the body of the request? Both approaches have advantages and drawbacks. Let’s compare them:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Passing the Tenant ID in the Header&lt;/strong&gt;&lt;br&gt;
✅ Pros:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Standardized: HTTP headers are commonly used for metadata (e.g., authentication tokens).&lt;/li&gt;
&lt;li&gt;Cleaner request bodies: The payload remains focused on business data.&lt;/li&gt;
&lt;li&gt;Abstracted from business logic: Tenant ID handling can be managed at the infrastructure level (e.g., middleware, filters), reducing code duplication.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;❌ Cons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Risk of header loss: In multi-service calls, headers might be unintentionally stripped or modified, making tenant context propagation unreliable.&lt;/li&gt;
&lt;li&gt;Requires explicit validation: You need additional checks to ensure the tenant ID is always present in each request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Passing the Tenant ID in the Body&lt;/strong&gt;&lt;br&gt;
✅ Pros:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Self-contained: The request body holds all necessary context, making it easier to debug.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;❌ Cons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requires modifying the data model for each service to include tenant information.&lt;/li&gt;
&lt;li&gt;GET request limitation: Since request bodies are not supported in GET requests, the tenant ID must be passed as a query parameter instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's look at how to easily abstract the tenant context from the rest of the business logic when using &lt;strong&gt;header&lt;/strong&gt; propagation of the tenant id.&lt;/p&gt;
&lt;h3&gt;
  
  
  REST
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;jakarta.servlet.http.HttpServletRequest&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;jakarta.servlet.http.HttpServletResponse&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.stereotype.Component&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.web.servlet.HandlerInterceptor&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TenantInterceptor&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;HandlerInterceptor&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="no"&gt;TENANT_HEADER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"X-Tenant-ID"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;preHandle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpServletRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;HttpServletResponse&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;tenantId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getHeader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;TENANT_HEADER&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenantId&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;TenantContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setTenantId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenantId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;afterCompletion&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpServletRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;HttpServletResponse&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;TenantContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clear&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Clean up after request processing&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;After registering the interceptor, we no longer need to explicitly retrieve the &lt;code&gt;tenant id&lt;/code&gt; or pass it between calls. It will be automatically set in the &lt;code&gt;TenantContext&lt;/code&gt; and work seamlessly.&lt;/p&gt;

&lt;p&gt;To also automate the propagation of the &lt;code&gt;tenant-id&lt;/code&gt; in outgoing REST calls, we can provide another interceptor and register it with &lt;code&gt;RestTemplate&lt;/code&gt;. This ensures successful tenant context propagation across multiple services without requiring any handling in the business logic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.http.HttpHeaders&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.http.HttpRequest&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.http.client.ClientHttpRequestExecution&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.http.client.ClientHttpRequestInterceptor&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.http.client.ClientHttpResponse&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.stereotype.Component&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.io.IOException&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TenantPropagationInterceptor&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ClientHttpRequestInterceptor&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="no"&gt;TENANT_HEADER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"X-Tenant-ID"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ClientHttpResponse&lt;/span&gt; &lt;span class="nf"&gt;intercept&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
                                        &lt;span class="nc"&gt;ClientHttpRequestExecution&lt;/span&gt; &lt;span class="n"&gt;execution&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;IOException&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;tenantId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TenantContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTenantId&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenantId&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getHeaders&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;TENANT_HEADER&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenantId&lt;/span&gt;&lt;span class="o"&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;execution&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Registering the interceptor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.boot.web.client.RestTemplateCustomizer&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.context.annotation.Bean&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.context.annotation.Configuration&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.web.client.RestTemplate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.util.List&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RestTemplateConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;RestTemplate&lt;/span&gt; &lt;span class="nf"&gt;restTemplate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TenantPropagationInterceptor&lt;/span&gt; &lt;span class="n"&gt;tenantInterceptor&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;RestTemplate&lt;/span&gt; &lt;span class="n"&gt;restTemplate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RestTemplate&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;restTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setInterceptors&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenantInterceptor&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;restTemplate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Kafka
&lt;/h3&gt;

&lt;p&gt;For Kafka, we can take a very similar approach by defining an interceptor that reads the message header and passes the value into the tenant context.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.apache.kafka.clients.consumer.ConsumerInterceptor&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.apache.kafka.clients.consumer.ConsumerRecords&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.apache.kafka.clients.consumer.ConsumerRecord&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.util.Map&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TenantConsumerInterceptor&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ConsumerInterceptor&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="no"&gt;TENANT_HEADER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"X-Tenant-ID"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ConsumerRecords&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;onConsume&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ConsumerRecords&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ConsumerRecord&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;lastHeader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;TENANT_HEADER&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;tenantId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;lastHeader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;TENANT_HEADER&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
                &lt;span class="nc"&gt;TenantContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setTenantId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenantId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="o"&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;records&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onCommit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt; &lt;span class="n"&gt;offsets&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// No action needed on commit&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Cleanup if needed&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;configs&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// No configuration needed&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We will also provide an interceptor that writes the &lt;code&gt;tenant-id&lt;/code&gt; into the header of outgoing messages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.apache.kafka.clients.producer.ProducerInterceptor&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.apache.kafka.clients.producer.ProducerRecord&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.apache.kafka.clients.producer.RecordMetadata&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.util.Map&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TenantProducerInterceptor&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ProducerInterceptor&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="no"&gt;TENANT_HEADER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"X-Tenant-ID"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ProducerRecord&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;onSend&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ProducerRecord&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;tenantId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TenantContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTenantId&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenantId&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;TENANT_HEADER&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenantId&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBytes&lt;/span&gt;&lt;span class="o"&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;record&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onAcknowledgement&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RecordMetadata&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// No action needed on acknowledgment&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Cleanup if needed&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;configs&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// No configuration needed&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Passing Tenant-Id from Ingress
&lt;/h2&gt;

&lt;p&gt;Tenants can have customized URLs specific to them. Our goal is to find an easy way to bind the URL of the tenant application to the &lt;code&gt;tenant-id&lt;/code&gt;. When running in Kubernetes, we can use the Ingress definition and set the header in the Nginx configuration.&lt;/p&gt;

&lt;p&gt;The following configuration sets the tenant ID in the header while also hiding any incoming &lt;code&gt;tenant-id&lt;/code&gt; headers from the request. Why do this?&lt;/p&gt;

&lt;p&gt;We want to ensure that clients cannot inject unexpected &lt;code&gt;tenant-id&lt;/code&gt;, ensuring that only our system assigns and propagates the correct &lt;code&gt;tenant-id&lt;/code&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;networking.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ingress&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example-ingress&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;nginx.ingress.kubernetes.io/configuration-snippet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;proxy_set_header X-Tenant-Id "tenant123";&lt;/span&gt;
      &lt;span class="s"&gt;proxy_hide_header X-Tenant-Id;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Splitting DB
&lt;/h2&gt;

&lt;p&gt;Splitting the database layer is the next logical step in building a secure multi-tenant architecture. We will not dive into the implementation details now; instead, we will explore how separation can be designed using the previously implemented components.&lt;/p&gt;

&lt;p&gt;By customizing the abstraction of our database layer—responsible for selecting the database connection and executing queries—we gain the ability to adapt this behavior and choose the appropriate database based on the current tenant context. This further separates multi-tenancy from the business logic, allowing the application to stay focused on business functionality rather than data separation.&lt;/p&gt;

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

&lt;p&gt;Multi-tenancy shouldn’t be something to fear. Following basic principles will open the door to better scaling of your applications and allow you to focus on business logic rather than the technical overhead of passing context information.&lt;/p&gt;

&lt;p&gt;The following diagram summarizes the article and illustrates the complete architecture that has been discussed.&lt;/p&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%2Fjl0s1kfxgk82b8jzqrck.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%2Fjl0s1kfxgk82b8jzqrck.png" alt="Image description" width="511" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let me know in the comments, what would you improve or what would you do differently?&lt;/p&gt;

</description>
      <category>kafka</category>
      <category>rest</category>
      <category>java</category>
      <category>microservices</category>
    </item>
    <item>
      <title>Building Real-Time Data Pipelines with Kafka Streams</title>
      <dc:creator>Martin Simon</dc:creator>
      <pubDate>Fri, 17 Jan 2025 12:33:17 +0000</pubDate>
      <link>https://dev.to/thesimdak/building-real-time-data-pipelines-with-kafka-streams-4535</link>
      <guid>https://dev.to/thesimdak/building-real-time-data-pipelines-with-kafka-streams-4535</guid>
      <description>&lt;p&gt;Microservice architecture may offer numerous benefits, but it also comes with its fair share of trade-offs. One of these is the challenge of managing distributed data across multiple databases. Providing even basic reporting can become a daunting task when data from various sources must be combined. For such scenarios, building a reporting service on top of a relational database can make a lot of sense. But how do you transfer the data efficiently without adding technical overhead with tools like Apache NiFi or Flink? In this article, I want to take a closer look at data streams using Kafka and discuss strategies to achieve this goal. I’d love to hear your thoughts and experiences as well.&lt;/p&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%2Fxppchzcz0u8aepb4iw5r.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%2Fxppchzcz0u8aepb4iw5r.png" alt="Image description" width="381" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Stream the Data, Not Just Events
&lt;/h2&gt;

&lt;p&gt;Consider a simple microservice for processing orders. An order can transition through the following states:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  CREATED
&lt;/li&gt;
&lt;li&gt;  PAID
&lt;/li&gt;
&lt;li&gt;  DELIVERING
&lt;/li&gt;
&lt;li&gt;  DELIVERED&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If we want to update the order data in the reporting system every time there’s a change, we need to listen to those changes. However, propagating data through streams should not rely solely on events but rather on the complete data itself. Avoid designing events that carry only partial state information about your domain.&lt;/p&gt;

&lt;p&gt;Here’s an example of a poorly designed event:&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;"specversion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OrderStatusUpdated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/orders-service"&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;"1234-5678-9012"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"time"&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-01-14T12:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"datacontenttype"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;        
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&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;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"order-001"&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;"SHIPPED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  
    &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&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-01-14T12:00:00Z"&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;The complexity of handling such events is significantly higher compared to handling data streams. Imagine a scenario where you want to restore the data warehouse by replaying all events from Kafka. With an event-per-status approach, the system would process each individual status change of an order, drastically increasing the load on the consumer. In contrast, handling complete data objects reduces the workload and ensures a more efficient pipeline.&lt;/p&gt;

&lt;p&gt;A simple solution is to use event structures that encapsulate the full state of the entity. For example:&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;"specversion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OrderCreateUpdate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/orders-service"&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;"event-5678"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"time"&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-01-14T13:05:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"datacontenttype"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&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;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"order-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"customer"&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;"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;"customer-123"&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 Doe"&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;"john.doe@example.com"&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;"items"&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="nl"&gt;"productId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prod-123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;20.0&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;"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;"SHIPPED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"statusHistory"&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="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;"PLACED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&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-01-14T10:00:00Z"&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;"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;"PROCESSING"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&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-01-14T11:00:00Z"&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;"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;"SHIPPED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&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-01-14T12:00:00Z"&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;"shippingDetails"&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;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"123 Main St, Springfield"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"carrier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DHL"&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;"totalAmount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;40.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lastUpdated"&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-01-14T12:00:00Z"&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;
  
  
  Idempotency: The Cornerstone of Data Consistency
&lt;/h2&gt;

&lt;p&gt;Let’s revisit the example of a poorly designed event that contains only a partial status update. Beyond the increased load on the consumer when restoring data, the critical flaw is the lack of idempotency in the event design.&lt;/p&gt;

&lt;p&gt;While the event includes an &lt;code&gt;id&lt;/code&gt;, the status update itself lacks a unique identifier. This creates challenges during consumption. Although consumers can verify whether they’ve already processed an event based on its &lt;code&gt;id&lt;/code&gt; and skip it if necessary, what happens if there’s an issue with the producer? Imagine the producer unintentionally generates multiple events for the same status change. Without a unique identifier for the status update, it becomes nearly impossible for consumers to determine whether the status has already been processed.&lt;/p&gt;

&lt;p&gt;This flaw can lead to data duplication during event processing, where the same change is applied multiple times, resulting in inconsistencies in the downstream system. For instance, in the case of a reporting database, duplicated events could inflate metrics, misrepresent totals, or even corrupt the overall dataset.&lt;/p&gt;

&lt;p&gt;To ensure idempotency, each event should carry a unique identifier not only for the event itself but also for the specific change it represents. This allows consumers to reliably deduplicate messages and maintain consistent data, even in the face of producer-side issues or message re-delivery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recreating Data in Your Data Store
&lt;/h2&gt;

&lt;p&gt;When it comes to initially loading data into a reporting system or restoring data for any reason, it’s essential to consider various strategies. Let’s explore some techniques to handle these scenarios effectively.&lt;/p&gt;

&lt;h3&gt;
  
  
  Replaying the Topic
&lt;/h3&gt;

&lt;p&gt;Resetting the offset of the Kafka consumer is the simplest way to retrieve all events from the beginning of the topic and restore data in your reporting system. However, this approach comes with several prerequisites and limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Retention Time&lt;/strong&gt;: The event retention time must be set to &lt;code&gt;-1&lt;/code&gt; (infinite retention) to ensure no messages are lost since the beginning. This is often impractical due to legal regulations or storage limitations.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Topic Existence&lt;/strong&gt;: The topic for exposing data must have existed since the inception of the order service. However, as systems evolve and functionalities are added iteratively, this is rarely the case in reality.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These constraints necessitate alternative solutions to ensure reliable data restoration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Republishing All Events
&lt;/h3&gt;

&lt;p&gt;One effective solution is to provide a mechanism to republish all events from scratch. This involves reading all data from the source database, transforming it into events, and publishing those events to the topic. Here’s a simple way to implement this:&lt;/p&gt;

&lt;h4&gt;
  
  
  Providing an Admin Endpoint
&lt;/h4&gt;

&lt;p&gt;Expose an admin endpoint that can trigger the republishing process. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /admin/expose-order-events
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This endpoint should have limited access, such as being protected behind authentication and authorization layers, to prevent misuse. When called, it starts reading data from the source database, transforms the data into events, and publishes them to Kafka.&lt;/p&gt;

&lt;h4&gt;
  
  
  Leveraging Idempotent Events
&lt;/h4&gt;

&lt;p&gt;If the events are designed to be idempotent, you can safely call this endpoint multiple times without causing data duplication or side effects. Idempotency ensures that the same event can be processed multiple times with consistent results, making this approach both reliable and robust.&lt;/p&gt;

&lt;p&gt;By combining these techniques, you can effectively manage data restoration scenarios while maintaining data consistency and reducing the complexity of your architecture.&lt;/p&gt;

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

&lt;p&gt;This article focused on key recommendations for implementing data streams using Kafka, intentionally avoiding the inclusion of additional tools to keep the architecture as straightforward as possible.&lt;/p&gt;

&lt;p&gt;By adhering to a few fundamental principles, you can significantly reduce future effort, minimize unnecessary load on consumers, and prevent potential data inconsistencies in your data warehouse.&lt;/p&gt;

&lt;p&gt;To achieve these goals, I recommend focusing on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Ensuring idempotency&lt;/strong&gt;: Design events with unique identifiers to avoid data duplication and maintain consistency.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Streaming full data objects, not just events&lt;/strong&gt;: Provide the complete state of domain entities to simplify downstream processing.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Enabling easy data restoration&lt;/strong&gt;: Ensure data can be easily restored from the source database to handle failures or rebuild the warehouse efficiently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By following these principles, you can create a robust and maintainable data pipeline that scales well and integrates seamlessly with your reporting systems.&lt;/p&gt;

&lt;p&gt;Let me know about your thoughts and opinions!&lt;/p&gt;

</description>
      <category>kafka</category>
      <category>microservices</category>
      <category>reporting</category>
      <category>database</category>
    </item>
  </channel>
</rss>
