<?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: Daniel Cordeiro</title>
    <description>The latest articles on DEV Community by Daniel Cordeiro (@dancodingbr).</description>
    <link>https://dev.to/dancodingbr</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%2F3942991%2Fb8febf1b-f02e-4022-8365-77175193d78c.png</url>
      <title>DEV Community: Daniel Cordeiro</title>
      <link>https://dev.to/dancodingbr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dancodingbr"/>
    <language>en</language>
    <item>
      <title>Building an AI Billing Assistant: Integrating LangChain ReAct Agents with Spring Boot Microservices</title>
      <dc:creator>Daniel Cordeiro</dc:creator>
      <pubDate>Mon, 08 Jun 2026 13:16:51 +0000</pubDate>
      <link>https://dev.to/dancodingbr/building-an-ai-billing-assistant-integrating-langchain-react-agents-with-spring-boot-microservices-4h74</link>
      <guid>https://dev.to/dancodingbr/building-an-ai-billing-assistant-integrating-langchain-react-agents-with-spring-boot-microservices-4h74</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;A significant portion of telecom customer support calls follow the same pattern: &lt;em&gt;What is my current bill?&lt;/em&gt;, &lt;em&gt;Why did it go up?&lt;/em&gt;, &lt;em&gt;I want to dispute this charge&lt;/em&gt;. These are structured, predictable requests with clear resolution paths — exactly the kind of interaction that a well-designed AI agent can handle reliably, without a human in the loop. &lt;/p&gt;

&lt;p&gt;This post presents the design and development of the &lt;em&gt;&lt;a href="https://github.com/dancodingbr/smart-billing-assistant" rel="noopener noreferrer"&gt;Smart Billing Assistant&lt;/a&gt;&lt;/em&gt;, an AI-powered telecom customer support agent that puts this idea into practice. A Python FastAPI service hosts a LangChain ReAct agent that gives customers a natural language interface to five self-service flows: viewing invoices, understanding bill changes, filing disputes, requesting plan changes, and checking payment status. Under the hood, the agent orchestrates two independent Java Spring Boot microservices — a reactive billing service (Spring WebFlux + R2DBC) and a transactional provisioning service (Spring MVC + JPA) — backed by PostgreSQL. The development lifecycle was shaped by spec-driven requirements, test-driven implementation and Claude Code as an AI pair programmer.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Project Overview
&lt;/h2&gt;

&lt;p&gt;&lt;small&gt;&lt;em&gt;&lt;strong&gt;Note:&lt;/strong&gt; The &lt;strong&gt;v1&lt;/strong&gt; term mentionated through this post is a demo version of the Smart Billing Assistant project, built for exploring purposes. Several design decisions throughout this document are explicitly simplified to keep scope manageable.&lt;/em&gt;&lt;/small&gt; &lt;/p&gt;

&lt;h3&gt;
  
  
  Why This Domain
&lt;/h3&gt;

&lt;p&gt;Business Support Systems (BSS) are the software backbone of a telecom company: billing, invoicing, customer accounts, payments. They are high-volume, data-intensive, and historically painful for customer support teams. A large portion of inbound support calls are about bill questions and simple service changes — exactly the kind of structured, predictable request that an AI agent handles well.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the Agent Can Do
&lt;/h3&gt;

&lt;p&gt;Six user stories were implemented:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;User Story&lt;/th&gt;
&lt;th&gt;What the customer says&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;US-01&lt;/td&gt;
&lt;td&gt;&lt;em&gt;What is my current bill?&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;The agent retrieves the customer's current invoice, including the billing summary and the individual line items that make up the total.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;US-02&lt;/td&gt;
&lt;td&gt;&lt;em&gt;Why is my bill so high?&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;The agent compares the current billing cycle against the prior one, identifies overages and one-time charges, and explains what drove the increase.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;US-03&lt;/td&gt;
&lt;td&gt;&lt;em&gt;I want to dispute this charge&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;The agent opens a dispute ticket for the specified charge, returns a unique reference number, and informs the customer that resolution takes up to 5 business days.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;US-04&lt;/td&gt;
&lt;td&gt;&lt;em&gt;Can I switch to a cheaper plan?&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;The agent verifies whether the customer's line is eligible for the requested plan and either applies the change immediately for upgrades or schedules it for the next billing cycle for downgrades.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;US-05&lt;/td&gt;
&lt;td&gt;&lt;em&gt;Did you receive my payment?&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;The agent returns the status of the customer's most recent payment — whether it was received, pending, or failed — together with the timestamp of the last update.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;US-06&lt;/td&gt;
&lt;td&gt;
&lt;em&gt;Why was it so high?&lt;/em&gt; (follow-up)&lt;/td&gt;
&lt;td&gt;The agent resolves the reference to &lt;em&gt;it&lt;/em&gt; from the active session context and answers without asking the customer to re-identify themselves or repeat previous information.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Fkhl4jcgociqchbc1w59i.gif" 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%2Fkhl4jcgociqchbc1w59i.gif" alt=" " width="760" height="805"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Architecture
&lt;/h3&gt;

&lt;p&gt;The system was decomposed into three independently deployable services:&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%2F768c37lgjx0zgpszgjcb.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%2F768c37lgjx0zgpszgjcb.png" alt=" " width="800" height="254"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;agent-service&lt;/strong&gt; (Python/FastAPI + LangChain): The sole customer entry point. Owns JWT validation, conversational session state, LangChain ReAct orchestration, and escalation logic. Calls the Java services over synchronous REST.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;billing-service&lt;/strong&gt; (Java/Spring WebFlux): The source of truth for invoices, line items, payments, and disputes. Uses reactive R2DBC for non-blocking database access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;provisioning-service&lt;/strong&gt; (Java/Spring MVC): The source of truth for plan catalogues, eligibility rules, and customer line configuration. Uses JPA/Hibernate with blocking I/O — plan changes are infrequent transactional writes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In summary, this is a holistic view of the project idea and the architecture it produced, and the following sections narrow the process in detail.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The Development Process
&lt;/h2&gt;

&lt;h3&gt;
  
  
  2.1 Using Claude Code as a Pair Programmer
&lt;/h3&gt;

&lt;p&gt;The entire project was built using Claude Code [1] — Anthropic's CLI-based AI coding agent — as an interactive pair programmer. Rather than treating it as a one-shot code generator, it was used as a persistent collaborator across 13 implementation sessions, each one driving a task from TDD flow (Red → Green → Refactor).&lt;/p&gt;

&lt;h4&gt;
  
  
  CLAUDE.md: The Project Instruction File
&lt;/h4&gt;

&lt;p&gt;The key to making Claude Code useful across sessions is the &lt;code&gt;CLAUDE.md&lt;/code&gt; file. This file lives in the project root and is automatically loaded by Claude Code at the start of every session. It acts as the project contract: what the system should do, what design principles to follow, what quality gates to enforce, and exactly what steps to execute after each task is completed.&lt;/p&gt;

&lt;p&gt;Depending on the project, a CLAUDE.md might include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Design principles&lt;/strong&gt;: KISS, YAGNI, AHA, SOLID — as concrete enforcement rules (e.g. &lt;em&gt;Don't add features beyond what was asked&lt;/em&gt;; &lt;em&gt;No Redis in v1 — YAGNI&lt;/em&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality gates&lt;/strong&gt;: ≥80% line coverage, SonarQube Maintainability Rating A, Cognitive Complexity ≤15 per method (≤10 for Python production code).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Task Completion Protocol&lt;/strong&gt;: An 8-step automated loop — implement → run tests → local validation (for &lt;code&gt;feat:&lt;/code&gt; tasks) → commit → open PR → monitor CI → fix failures → report green.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Git conventions&lt;/strong&gt;: Conventional Commits required; Semantic Release handles versioning; no manual version bumps in &lt;code&gt;pom.xml&lt;/code&gt; or &lt;code&gt;pyproject.toml&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Why Start Simple and Evolve
&lt;/h4&gt;

&lt;p&gt;A comprehensive &lt;code&gt;CLAUDE.md&lt;/code&gt; was not written upfront. The project started with a minimal version — basic rules about TDD and commit conventions — and expanded it as new needs emerged during actual development. So, each &lt;code&gt;CLAUDE.md&lt;/code&gt; addition was prompted by a real friction point, evolving the process incrementally, driven by actual need.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why Document Artifacts Instead of Prompting
&lt;/h4&gt;

&lt;p&gt;One of the highest-leverage decisions was treating &lt;code&gt;PROJECT_IDEA.md&lt;/code&gt;, &lt;code&gt;REQUIREMENTS.md&lt;/code&gt;, &lt;code&gt;DESIGN.md&lt;/code&gt;, and &lt;code&gt;TASKS.md&lt;/code&gt; as first-class project artifacts — files Claude Code reads directly rather than content repeated in every chat prompt.&lt;/p&gt;

&lt;p&gt;Benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Token efficiency&lt;/strong&gt;: The context is loaded once from stable files, not repeated in every session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistency&lt;/strong&gt;: The same scope, terminology, and decisions are visible in every session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guardrails&lt;/strong&gt;: Claude Code stays bounded by what is documented; speculative features don't creep in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory&lt;/strong&gt;: Session history notes in the docs capture every design decision — when returning to a task, the rationale is already there.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  2.2 Defining the Requirements
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Spec-Driven Development
&lt;/h4&gt;

&lt;p&gt;Before writing a single line of code, &lt;code&gt;REQUIREMENTS.md&lt;/code&gt; was produced: user stories with explicit acceptance scenarios for every state the system needs to handle. This approach is inspired by spec-driven development tools like Kiro [2] — requirements come first, and the code is tested against them.&lt;/p&gt;

&lt;p&gt;To ground the requirements in real domain knowledge, Claude Code was asked to adopt the role of a Billing Manager stakeholder with expertise in BSS telecom. This simulated discovery session drove a structured discussion: what do customers actually call about? What are the edge cases? What is in scope and what crosses a line the support agent should not cross? The conversation surfaced business rules (the 90-day dispute window, the delinquency threshold, the OSS/BSS boundary) that would otherwise have been discovered late — during implementation or testing.&lt;/p&gt;

&lt;p&gt;The format: each user story has an &lt;em&gt;As a / I want / So that&lt;/em&gt; header, followed by numbered acceptance scenarios (S1, S2, S3...) that map directly to test cases and eventually to BDD Gherkin feature files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example (US-03 — Dispute a Charge):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;US-03: As a customer, I want to dispute a charge on my invoice,
       so that I can get incorrect charges reviewed.

S1 — Valid dispute filed: Customer provides invoice ID and line item ID.
     System creates a dispute record and returns a reference number
     with a 5-business-day resolution SLA.

S2 — Duplicate dispute blocked: An open dispute already exists for
     that line item. System returns the existing reference number
     instead of creating a second dispute.

S3 — Outside 90-day window: The charge is more than 90 days old.
     System rejects the request and explains the eligibility window.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These scenarios drove every test: unit tests mocked the repository layer and asserted each branch; integration tests against Testcontainers PostgreSQL confirmed end-to-end behavior; BDD feature files in Cucumber (Java) and pytest-bdd (Python) verified full conversation flows.&lt;/p&gt;

&lt;h4&gt;
  
  
  GLOSSARY.md: Capturing the Domain
&lt;/h4&gt;

&lt;p&gt;One output of the requirements session was &lt;code&gt;GLOSSARY.md&lt;/code&gt; — a glossary of BSS telecom terms. Proration, CDR, dunning, delinquency, cold handoff — these terms appear in the code, in tests, and in the agent's responses. Having a shared glossary ensures that when the code says &lt;code&gt;OUTSTANDING&lt;/code&gt; or the agent mentions &lt;em&gt;5-business-day SLA&lt;/em&gt;, it means the same thing to everyone reading it.&lt;/p&gt;




&lt;h3&gt;
  
  
  2.3 Defining the Design
&lt;/h3&gt;

&lt;h4&gt;
  
  
  LangChain, LangGraph, and the ReAct Pattern
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;LangChain&lt;/strong&gt; [3] is a Python framework for building applications powered by LLMs. Its core concept is the &lt;strong&gt;tool&lt;/strong&gt;: a Python function the LLM can call to take actions or retrieve information. The LLM decides which tool to call, what arguments to pass, and what to do with the result.&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%2F2dth9j1w2v8vpsxl04u3.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%2F2dth9j1w2v8vpsxl04u3.png" alt=" " width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each tool is a plain Python function decorated with &lt;code&gt;@tool&lt;/code&gt;. The LLM reads the function's docstring to understand when and how to use it — no routing tables, no decision trees. LangChain handles the mechanics of formatting the tool call, parsing the LLM's response, and invoking the function.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ReAct&lt;/strong&gt; [4] (Reason + Act) is the agent pattern used here. The LLM alternates between:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reasoning&lt;/strong&gt;: &lt;em&gt;The customer asked about their bill. I should call &lt;code&gt;get_current_invoice&lt;/code&gt;.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Acting&lt;/strong&gt;: Call the tool, get the result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observing&lt;/strong&gt;: &lt;em&gt;The invoice shows a $45 data overage. The customer needs an explanation.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reasoning again&lt;/strong&gt;: Do I need more information, or can I answer now?&lt;/li&gt;
&lt;/ol&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%2Fjhxv94gzmvz16wwsdgjg.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%2Fjhxv94gzmvz16wwsdgjg.png" alt=" " width="800" height="1101"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This loop runs entirely inside the agent-service. From the customer's perspective, they send a message and receive a reply. Inside, the LLM may have called two or three tools, observed intermediate results, and reasoned about each before generating the final response.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LangGraph&lt;/strong&gt; adds state management to this loop. It is a graph-based runtime where nodes are functions (like &lt;em&gt;call the LLM&lt;/em&gt; or &lt;em&gt;execute a tool call&lt;/em&gt;) and edges control flow. Crucially, it provides a &lt;strong&gt;MemorySaver checkpointer&lt;/strong&gt; that persists conversation history between turns — keyed by a &lt;code&gt;thread_id&lt;/code&gt;. This is what gives the agent multi-turn memory.&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%2Fvz1frey1eytxnuonv3h4.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%2Fvz1frey1eytxnuonv3h4.png" alt=" " width="800" height="590"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;thread_id&lt;/code&gt; is the session UUID. On every &lt;code&gt;POST /chat&lt;/code&gt; request, the agent loads the conversation history for that thread, runs the ReAct loop, and saves the updated state — including the new user message, any tool calls, and the final AI response. The customer's next message picks up exactly where the last one left off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LangSmith&lt;/strong&gt; traces every ReAct loop: which tools were called, what the LLM reasoned at each step, how long each operation took. Invaluable for debugging agent behavior.&lt;/p&gt;

&lt;h4&gt;
  
  
  Spring WebFlux + R2DBC and Spring MVC + JPA
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Spring WebFlux + R2DBC&lt;/strong&gt; powers the billing-service. WebFlux is Spring's reactive web framework [5]: instead of one blocking thread per HTTP request, it uses a small, fixed thread pool with event-loop-based I/O. Requests are represented as non-blocking streams (&lt;code&gt;Mono&amp;lt;T&amp;gt;&lt;/code&gt; for a single value, &lt;code&gt;Flux&amp;lt;T&amp;gt;&lt;/code&gt; for a sequence). R2DBC (Reactive Relational Database Connectivity) is the reactive counterpart to JDBC — database queries return &lt;code&gt;Mono&lt;/code&gt; or &lt;code&gt;Flux&lt;/code&gt; publishers rather than blocking the calling thread. This stack is a good fit for the billing-service because invoice lookups, comparison queries, and payment status checks are all read-heavy operations that hit the database frequently and concurrently.&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%2Fay2qto94suc5sno377op.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%2Fay2qto94suc5sno377op.png" alt=" " width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spring MVC + JPA&lt;/strong&gt; powers the provisioning-service. Spring MVC is the classic, blocking web framework [6]: one thread per request, synchronous database calls through JPA/Hibernate. This is the choice for the provisioning-service because plan changes are low-frequency, write-heavy transactional operations — the simplicity of blocking code outweighs any throughput benefit from reactive streams. JPA's entity mapping and transaction management make the write path straightforward to reason about and test.&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%2F06j5ztnz5qp1thr2iot5.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%2F06j5ztnz5qp1thr2iot5.png" alt=" " width="800" height="221"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prometheus + Grafana Observability:&lt;/strong&gt; Both Java services expose metrics through the Micrometer instrumentation library, which is included with Spring Boot Actuator. Prometheus scrapes the &lt;code&gt;/actuator/prometheus&lt;/code&gt; endpoint on both services and stores the metrics as time-series data. Grafana connects to Prometheus as a datasource and visualises the data in dashboards.&lt;/p&gt;

&lt;h4&gt;
  
  
  Key Design Decisions
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Chosen&lt;/th&gt;
&lt;th&gt;Alternative&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Language split&lt;/td&gt;
&lt;td&gt;Python + Java&lt;/td&gt;
&lt;td&gt;Monolith (either)&lt;/td&gt;
&lt;td&gt;LangChain is Python-first; Spring WebFlux is battle-tested for high-volume BSS data. Each language serves the layer where it is strongest, and that domain alignment justifies the operational complexity of running two runtimes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent pattern&lt;/td&gt;
&lt;td&gt;LangChain ReAct&lt;/td&gt;
&lt;td&gt;Structured routing&lt;/td&gt;
&lt;td&gt;A structured router requires every customer intent to be hardcoded upfront. ReAct lets the LLM reason dynamically across multi-step billing queries.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session state&lt;/td&gt;
&lt;td&gt;LangGraph in-process&lt;/td&gt;
&lt;td&gt;Redis&lt;/td&gt;
&lt;td&gt;Redis adds a new infrastructure dependency, serialisation, and failure handling for no v1 benefit. Session loss on restart is accepted at this scale. Redis is the natural v2 step when horizontal scaling is needed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inter-service&lt;/td&gt;
&lt;td&gt;REST (sync)&lt;/td&gt;
&lt;td&gt;Message queues&lt;/td&gt;
&lt;td&gt;The customer waits in real time — async messaging would require correlation IDs and timeout handling for an inherently synchronous interaction. REST gives predictable latency and simple error propagation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disputes&lt;/td&gt;
&lt;td&gt;Flag-only&lt;/td&gt;
&lt;td&gt;Auto-reversal&lt;/td&gt;
&lt;td&gt;Auto-reversal requires a Revenue Assurance approval workflow that is outside the agent's authority and out of v1 scope. The agent captures the claim and issues a reference number; the reversal decision stays with a human reviewer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JWT validation&lt;/td&gt;
&lt;td&gt;agent-service boundary&lt;/td&gt;
&lt;td&gt;API Gateway&lt;/td&gt;
&lt;td&gt;JWT validation was placed at the FastAPI boundary rather than in a dedicated API Gateway because the agent-service is the only external-facing service in v1. Single external-facing service makes a dedicated gateway YAGNI for v1.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database type&lt;/td&gt;
&lt;td&gt;PostgreSQL for billing and provisioning services&lt;/td&gt;
&lt;td&gt;MongoDB for provisioning&lt;/td&gt;
&lt;td&gt;Eligibility checks depend on structured SQL queries across &lt;code&gt;plans&lt;/code&gt; and &lt;code&gt;customer_lines&lt;/code&gt;. The array columns and event-log pattern in provisioning create minor relational friction but do not outweigh the operational cost of running a second database technology at v1 scale. MongoDB is the natural revisit if the plan catalogue grows to support dozens of configurable attributes per tier.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h3&gt;
  
  
  2.4 Executing the Tasks
&lt;/h3&gt;

&lt;h4&gt;
  
  
  TASKS.md: From Design to Executable Work
&lt;/h4&gt;

&lt;p&gt;The final design output was &lt;code&gt;TASKS.md&lt;/code&gt;: 13 tasks, each decomposed into sub-tasks, each sub-task referencing specific user story scenarios. TDD within each task followed the same rhythm: write a failing unit test, implement just enough to pass, write a failing integration test, implement until it passes, then refactor.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Three Services and Their Roles: Putting It All Together
&lt;/h4&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%2Fny2mlu6gfmx4wcornuso.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%2Fny2mlu6gfmx4wcornuso.png" alt=" " width="800" height="691"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;agent-service&lt;/strong&gt; is the sole customer-facing entry point. It validates JWT tokens, creates and manages conversational sessions, and runs the LangChain ReAct agent loop. When a customer sends a message, the agent reasons about intent, calls whichever billing or provisioning tool is needed, observes the result, and formulates a natural language response.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;billing-service&lt;/strong&gt; is the source of truth for all financial data. It owns invoices and their line items, payment records, and dispute tickets. It is the only service that knows whether a customer's account is active or suspended, and whether their balance is overdue. Its reactive stack (Project Reactor + R2DBC) handles high read volumes — invoice queries, comparison lookups, payment status checks — without blocking server threads.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;provisioning-service&lt;/strong&gt; owns the plan catalogue, eligibility rules, and each customer's current line configuration. It decides which plans a customer can switch to (based on network capability flags and regional availability) and applies or schedules the resulting plan change.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. Conclusion
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Each language in its own domain.&lt;/strong&gt; Python is the natural home for LangChain. Java is proven for high-volume transactional services. Combining them means the agent layer and the data layer each run in the ecosystem where they are best supported.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Spec-driven development pays off at test time.&lt;/strong&gt; Producing REQUIREMENTS.md with explicit acceptance scenarios before touching the code makes test-writing focused. Every scenario has a name, a precondition, and an expected outcome.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ReAct agents are powerful but need guardrails.&lt;/strong&gt; The ReAct loop gives the LLM significant autonomy — it decides which tool to call, in what order, and when to stop. For billing queries, this flexibility is valuable: a customer asking &lt;em&gt;why did my bill change?&lt;/em&gt; may require the agent to call two tools and reason about both results before answering — a flow that would be brittle to hardcode. But autonomy introduces risk: the LLM could call a write tool (like &lt;code&gt;file_dispute&lt;/code&gt;) when the customer only asked a question. The tool closure pattern (capturing &lt;code&gt;customer_id&lt;/code&gt; in the closure, returning status-keyed dicts instead of raising exceptions) and the clear separation of read and write tools are the guardrails that keep the agent predictable. LangSmith traces make any misbehavior visible and debuggable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Load the customer's data once at session start.&lt;/strong&gt; Fetching the customer's invoice and eligible plans at session creation makes every follow-up question in the conversation instantaneous and natural. The customer does not need to repeat their account details on every follow-up message, because the agent already has the relevant data loaded from the moment the session opened.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CLAUDE.md as a living document.&lt;/strong&gt; The instruction file that guides an AI pair programmer grows alongside the project. Each friction point or new decision is an opportunity to add a rule that prevents the same issue from recurring.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Trade-offs and Limitations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Session loss on restart&lt;/strong&gt;: LangGraph's MemorySaver stores all conversation state in the agent-service process memory. When the process restarts — during a deployment, a crash, or a container restart — every active session is immediately lost. A customer mid-conversation would receive a &lt;em&gt;session not found&lt;/em&gt; error and have to start over from scratch. For v1, where there is a single process and restarts are infrequent, this is acceptable. In a multi-instance production setup, it becomes a hard blocker: a session created on instance A would not be visible to instance B, making load balancing impossible without sticky sessions. Redis would solve this by persisting each conversation turn to a shared external store, making sessions portable across instances and restart-safe.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;JWT validation at the application boundary&lt;/strong&gt;: In a proper production microservices architecture, JWT validation is the responsibility of an API Gateway (Kong, AWS API Gateway, nginx with an auth module, etc.). The gateway validates the token, extracts verified claims, and forwards them as trusted headers (e.g., &lt;code&gt;X-Customer-Id&lt;/code&gt;) to services behind it. In v1, this responsibility was placed directly in the FastAPI boundary because the agent-service is the only external-facing service — it effectively acts as its own gateway, making a dedicated one YAGNI. The problem surfaces when the system grows: a second external-facing service would need to duplicate the same JWT logic, and rotating the signing key or changing the token format would require updating every service that validates tokens.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Delinquency as synchronous cross-service call&lt;/strong&gt;: When a customer requests a plan change, the provisioning-service makes a blocking HTTP call to the billing-service to check whether the account has an overdue balance. This creates a direct runtime dependency between the two services: if the billing-service is slow or temporarily unavailable, the provisioning plan change endpoint is also degraded — even though plan logic has nothing to do with invoice processing. For v1 with a small user base, this coupling is manageable. At higher volume, the right approach is an event-driven model: the billing-service publishes an account status event when delinquency is detected, and the provisioning-service maintains a local read-model of account statuses updated from those events. Plan change requests then query the local cache — no synchronous cross-service call needed at runtime.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Single PostgreSQL instance&lt;/strong&gt;: The &lt;code&gt;billing&lt;/code&gt; and &lt;code&gt;provisioning&lt;/code&gt; schemas both live inside one PostgreSQL container in Docker Compose. While the application code enforces strict schema separation (no cross-schema queries, no shared tables), they share the same database process, disk I/O, connection pool, and resource limits. A slow billing query can starve provisioning reads. In production, each service should own its own PostgreSQL instance — separate containers, separate data volumes, potentially separate machines — so that they can be tuned, backed up, scaled, and failed over independently.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Relational database for both services&lt;/strong&gt;: The billing-service is unambiguous — financial records, ACID guarantees, complex aggregation queries across billing cycles map naturally to relational. The provisioning-service is less clear-cut: the plans table uses array columns (regions[], network_flags) and plan_changes is effectively an event log — both patterns that a document store handles more naturally. For v1, eligibility checks benefit from structured SQL queries, KISS applies, and the operational cost of introducing a second database technology outweighs the schema flexibility it would bring at this scale. If the plan catalogue grows to support dozens of configurable attributes per tier, MongoDB would be the natural migration path for the provisioning-service's plan data.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Source code: &lt;a href="https://github.com/dancodingbr/smart-billing-assistant" rel="noopener noreferrer"&gt;github.com/dancodingbr/smart-billing-assistant&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;&lt;a id="ref-1"&gt;&lt;/a&gt;[1] Claude Code — Anthropic's CLI-based AI coding agent. Available at: &lt;a href="https://github.com/anthropics/claude-code" rel="noopener noreferrer"&gt;https://github.com/anthropics/claude-code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="ref-2"&gt;&lt;/a&gt;[2] Kiro — AWS AI-powered IDE built around spec-driven development. Available at: &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;https://kiro.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="ref-3"&gt;&lt;/a&gt;[3] LangChain — Framework for building LLM-powered applications. Available at: &lt;a href="https://python.langchain.com" rel="noopener noreferrer"&gt;https://python.langchain.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="ref-4"&gt;&lt;/a&gt;[4] ReAct: Synergizing Reasoning and Acting in Language Models — Yao et al., 2022. Available at: &lt;a href="https://arxiv.org/abs/2210.03629" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2210.03629&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="ref-5"&gt;&lt;/a&gt;[5] Spring WebFlux — Reactive web framework built on Project Reactor. Available at: &lt;a href="https://docs.spring.io/spring-framework/reference/web/webflux.html" rel="noopener noreferrer"&gt;https://docs.spring.io/spring-framework/reference/web/webflux.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="ref-6"&gt;&lt;/a&gt;[6] Spring Boot — Convention-over-configuration framework for Java microservices. Available at: &lt;a href="https://spring.io/projects/spring-boot" rel="noopener noreferrer"&gt;https://spring.io/projects/spring-boot&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>langchain</category>
      <category>python</category>
      <category>java</category>
    </item>
    <item>
      <title>How Terraform and Helm Split Responsibilities in a Kubernetes CI/CD Pipeline</title>
      <dc:creator>Daniel Cordeiro</dc:creator>
      <pubDate>Tue, 02 Jun 2026 14:36:19 +0000</pubDate>
      <link>https://dev.to/dancodingbr/how-terraform-and-helm-split-responsibilities-in-a-kubernetes-cicd-pipeline-h59</link>
      <guid>https://dev.to/dancodingbr/how-terraform-and-helm-split-responsibilities-in-a-kubernetes-cicd-pipeline-h59</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Anyone working with Kubernetes for a while will likely face a version of the same question: should Kubernetes resources be managed through Terraform, or through something else?&lt;/p&gt;

&lt;p&gt;In addition to provisioning the cluster, Terraform has a mature &lt;code&gt;kubernetes&lt;/code&gt; provider that exposes namespaces, Deployments, StatefulSets, and ConfigMaps as first-class resources. Everything can live in the same state file, the same repository, and the same workflow. For teams that already operate Terraform for infrastructure, the case for extending that coverage to application workloads is genuinely strong.&lt;/p&gt;

&lt;p&gt;The problem is that infrastructure and application workloads have fundamentally different change rates. A Kubernetes namespace or a monitoring stack might remain untouched for months. A container image changes with every commit. This is the gap Helm was designed to fill: a dedicated tool for packaging, versioning, and deploying application workloads into Kubernetes, with parameterized overrides, release history, and rollback built in.&lt;/p&gt;

&lt;p&gt;To make this concrete, this article documents a hands-on project named &lt;a href="https://gitlab.com/dancodingbr/personal-blog" rel="noopener noreferrer"&gt;Personal Blog&lt;/a&gt; that explores both scenarios, and how it evolved from the monolithic Stage 2 — where Terraform managed both infrastructure provisioning and application deployment — to the decoupled Stage 3 - a clean separation of concerns where Terraform owns the platform layer and Helm owns the application layer, orchestrated by a GitLab CI/CD pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Temptation of "Terraform Everything"
&lt;/h2&gt;

&lt;p&gt;In Stage 2 of the personal blog CI/CD pipeline, a decision was made that seemed reasonable at the time: use Terraform for everything. Not just the cluster — everything. The Kubernetes namespace, MongoDB StatefulSet, backend and frontend Deployments, Services, ConfigMaps for Prometheus and Grafana, the entire monitoring stack. One tool, one state file, one workflow.&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%2Fstqf07letxvf4ad0ia08.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%2Fstqf07letxvf4ad0ia08.png" alt="Stage 2 Diagram" width="800" height="599"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's what the &lt;code&gt;terraform apply&lt;/code&gt; workflow covered in Stage 2:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Provisioning a Kind (Kubernetes in Docker) cluster with the &lt;code&gt;tehcyx/kind&lt;/code&gt; provider;&lt;/li&gt;
&lt;li&gt;Generating kubeconfig and loading local Docker images into the cluster registry using a Provisioner;&lt;/li&gt;
&lt;li&gt;Creating Kubernetes namespace with labels;&lt;/li&gt;
&lt;li&gt;Deploying MongoDB as a &lt;code&gt;kubernetes_stateful_set&lt;/code&gt; with a PersistentVolumeClaim;&lt;/li&gt;
&lt;li&gt;Deploying Spring Boot backend: &lt;code&gt;kubernetes_deployment&lt;/code&gt; with 2 replicas, resource limits (500m CPU / 512Mi), NodePort service;&lt;/li&gt;
&lt;li&gt;Deploying Angular frontend: &lt;code&gt;kubernetes_deployment&lt;/code&gt; with 2 replicas, resource limits (300m CPU / 256Mi), NodePort service;&lt;/li&gt;
&lt;li&gt;Deploying Prometheus, Loki, Promtail DaemonSet, and Grafana with pre-configured datasources and dashboards via &lt;code&gt;kubernetes_config_map&lt;/code&gt;;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of this in a single &lt;code&gt;local_kubernetes.tf&lt;/code&gt; file — over 1,000 lines.&lt;/p&gt;

&lt;p&gt;Look closely at what those 1,000 lines actually contain: Kubernetes Deployments, StatefulSets, Services, ConfigMaps, and DaemonSets — written in HCL syntax instead of YAML. In other words, the file is a collection of &lt;strong&gt;implicit Kubernetes manifests embedded inside Terraform&lt;/strong&gt;. Every resource that would normally be a few lines of YAML becomes a deeply nested HCL block. Each one is hardcoded — image tags, replica counts, resource limits, service ports, environment variables all written inline, duplicated wherever they appear, and entirely specific to one environment.&lt;/p&gt;

&lt;p&gt;This is precisely a gap that Helm can fit to close. Without it, you end up with exactly what Stage 2 produced: large, unmaintainable manifests with no reusability across environments, no parameterization strategy, and no mechanism to version or roll back the application as a deployable unit.&lt;/p&gt;

&lt;p&gt;It ran. But every time the application changed — a new Docker image, a backend configuration update — the only way to deploy was &lt;code&gt;terraform apply&lt;/code&gt;. Which means every code change ran through a tool designed for &lt;em&gt;infrastructure provisioning&lt;/em&gt;, not &lt;em&gt;application delivery&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Terraform Can Manage Kubernetes Resources at All
&lt;/h2&gt;

&lt;p&gt;Terraform is built around a provider plugin model [1]. A provider is a plugin that translates Terraform resource definitions into API calls for a specific platform. HashiCorp publishes official providers for AWS, Azure, GCP, and Kubernetes. Third parties publish providers for everything else — in this project, the &lt;code&gt;tehcyx/kind&lt;/code&gt; provider was used to provision the Kind cluster itself.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;hashicorp/kubernetes&lt;/code&gt; provider [2] is what makes managing Kubernetes resources declaratively with Terraform possible. It exposes Kubernetes API objects — Namespaces, Deployments, Services, ConfigMaps, StatefulSets, Secrets, RBAC resources — as first-class Terraform resources [3]. Under the hood, it authenticates against the Kubernetes API server using the configured kubeconfig and issues the equivalent of &lt;code&gt;kubectl apply&lt;/code&gt; calls, but managed through Terraform's state model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;kubernetes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/kubernetes"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2.38.0"&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="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"kubernetes"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;config_path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pathexpand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kubeconfig_path&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;From that point on, any Kubernetes object can be declared as a Terraform resource:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"kubernetes_deployment"&lt;/span&gt; &lt;span class="s2"&gt;"backend"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;metadata&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"personal-blog-backend"&lt;/span&gt;
    &lt;span class="nx"&gt;namespace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;kubernetes_namespace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metadata&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="nx"&gt;name&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;spec&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;replicas&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="nx"&gt;selector&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;match_labels&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"personal-blog-backend"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;template&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;metadata&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;labels&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"personal-blog-backend"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;spec&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;container&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"backend"&lt;/span&gt;
          &lt;span class="nx"&gt;image&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"dancodingbr/personal-blog-backend:latest"&lt;/span&gt;
          &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;limits&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cpu&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"500m"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;memory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"512Mi"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nx"&gt;requests&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cpu&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"250m"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;memory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"256Mi"&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="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 means that Terraform can manage the entire lifecycle of a Kubernetes cluster &lt;em&gt;and&lt;/em&gt; the workloads running inside it, from a single configuration and state file.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Core Problem: Two Different Change Rates
&lt;/h2&gt;

&lt;p&gt;Terraform is designed around the assumption that infrastructure changes infrequently. You create a VPC, a database cluster, a Kubernetes namespace — and those things persist for months or years. Terraform's state model, plan/apply cycle, and provider ecosystem are all optimized for this cadence.&lt;/p&gt;

&lt;p&gt;Application deployments have a completely different change rate. A development team might deploy multiple times per day. The image tag changes with every commit. Replica counts get tuned. Environment variables get updated. These are not infrastructure events — they are application lifecycle events.&lt;/p&gt;

&lt;p&gt;When you run &lt;code&gt;terraform apply&lt;/code&gt; to update an image tag, you're running the full plan/apply cycle — provider initialization, state refresh, dependency graph evaluation — just to change one line in a Deployment spec. It's slow, it carries the risk of unintentional side-effects on other resources in the state file, and it conflates two fundamentally different concerns.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution: Separation of Concerns
&lt;/h2&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%2Fzafn0pg8lelu0f2kgq2w.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%2Fzafn0pg8lelu0f2kgq2w.png" alt="Stage 3 Diagram" width="786" height="715"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Stage 3 of the project refactors this cleanly. Terraform keeps exactly two responsibilities:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Kubernetes namespace creation.&lt;/li&gt;
&lt;li&gt;Helm releases [4] for platform-level services: MongoDB and the monitoring stack (Prometheus, Loki, Promtail, Grafana).
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# terraform/helm/kubernetes.tf&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"kubernetes_namespace"&lt;/span&gt; &lt;span class="s2"&gt;"personal_blog_namespace"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;metadata&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app_namespace&lt;/span&gt;
    &lt;span class="nx"&gt;labels&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app_labels&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# terraform/helm/helm.tf&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"helm_release"&lt;/span&gt; &lt;span class="s2"&gt;"mongodb"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"mongodb-release"&lt;/span&gt;
  &lt;span class="nx"&gt;namespace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app_namespace&lt;/span&gt;
  &lt;span class="nx"&gt;chart&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${path.module}/../../charts/mongodb"&lt;/span&gt;
  &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;kubernetes_namespace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;personal_blog_namespace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"helm_release"&lt;/span&gt; &lt;span class="s2"&gt;"monitoring"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"monitoring-release"&lt;/span&gt;
  &lt;span class="nx"&gt;namespace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app_namespace&lt;/span&gt;
  &lt;span class="nx"&gt;chart&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${path.module}/../../charts/monitoring"&lt;/span&gt;
  &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;kubernetes_namespace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;personal_blog_namespace&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 frontend and backend are removed from Terraform entirely. They become Helm chart releases managed by the GitLab CI pipeline — deployed with &lt;code&gt;helm upgrade --install&lt;/code&gt; and a dynamic image tag override:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm upgrade &lt;span class="nt"&gt;--install&lt;/span&gt; personal-blog-backend-release &lt;span class="se"&gt;\&lt;/span&gt;
  ./charts/personal-blog-backend &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; image.repository&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"dancodingbr/personal-blog-backend"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; image.tag&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CI_COMMIT_SHORT_SHA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; personal-blog-app-dev &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--wait&lt;/span&gt; &lt;span class="nt"&gt;--timeout&lt;/span&gt; 90s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this way, Terraform runs infrequently to provision the platform. Helm runs on every deploy to update the application.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Result: A Clean Responsibility Matrix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Change frequency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Kubernetes namespace&lt;/td&gt;
&lt;td&gt;Terraform&lt;/td&gt;
&lt;td&gt;Rarely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MongoDB deployment&lt;/td&gt;
&lt;td&gt;Terraform + Helm&lt;/td&gt;
&lt;td&gt;Rarely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monitoring stack&lt;/td&gt;
&lt;td&gt;Terraform + Helm&lt;/td&gt;
&lt;td&gt;Rarely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend deployment&lt;/td&gt;
&lt;td&gt;Helm (via GitLab CI)&lt;/td&gt;
&lt;td&gt;Every commit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend deployment&lt;/td&gt;
&lt;td&gt;Helm (via GitLab CI)&lt;/td&gt;
&lt;td&gt;Every commit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What Helm Gives You For App Deployments
&lt;/h2&gt;

&lt;p&gt;Kubernetes manifests can become large and repetitive, hard to parameterize across environments, and difficult to version and reuse. A simple application — a Deployment, a Service, a ConfigMap — might span dozens of YAML files, each needing different values for dev, staging, and production. Helm's answer was Charts: reusable, templated application packages with a clean variable model.&lt;/p&gt;

&lt;p&gt;The 1,000-line &lt;code&gt;local_kubernetes.tf&lt;/code&gt; from Stage 2 is a perfect illustration of what happens when that problem goes unsolved. Every &lt;code&gt;kubernetes_deployment&lt;/code&gt;, &lt;code&gt;kubernetes_service&lt;/code&gt;, and &lt;code&gt;kubernetes_config_map&lt;/code&gt; block in that file is a hardcoded, single-environment, non-reusable Terraform approximation of a Kubernetes manifest. Helm would have expressed the same application as a handful of templated files and a single &lt;code&gt;values.yaml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The capabilities Helm [5] introduces to solve this are exactly the ones Terraform's Kubernetes provider lacks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Chart packaging and environment parameterization&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A Helm chart separates &lt;em&gt;what&lt;/em&gt; to deploy (the templates) from &lt;em&gt;how&lt;/em&gt; to configure it (the values). The same chart deploys to dev, staging, and production by swapping the values file — no duplication, no environment-specific HCL blocks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Parameterized overrides at deploy time&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Helm's &lt;code&gt;values.yaml&lt;/code&gt; + &lt;code&gt;--set&lt;/code&gt; overrides are designed for the pattern of "here's a base config, override what changes per deploy." In CI/CD, this is how image tags are injected at runtime without modifying source files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Release management&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;helm list&lt;/code&gt; gives you a clean view of what version of each chart is deployed, in which namespace, with which revision. Rolling back is &lt;code&gt;helm rollback &amp;lt;release&amp;gt; &amp;lt;revision&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Immutable, traceable deployments&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Using &lt;code&gt;--set image.tag="$CI_COMMIT_SHORT_SHA"&lt;/code&gt; means every deployment is tied to a specific Git commit. Combined with &lt;code&gt;helm history&lt;/code&gt;, you have a full audit trail.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;One goal of DevOps is not to minimize the number of tools. It is to give each concern the tool that fits it best. So, when designing a Kubernetes-based deployment pipeline from scratch, the following separation of concerns is suggested:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Terraform for:&lt;/strong&gt; namespaces, persistent infrastructure services (databases, message queues), RBAC, cluster-level resources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Helm for:&lt;/strong&gt; application workloads — anything that has a &lt;code&gt;Deployment&lt;/code&gt;, scales independently, and is updated frequently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD tool for:&lt;/strong&gt; sequencing them correctly — run Terraform first (idempotently), then run Helm for the changed application.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Source code: &lt;a href="https://gitlab.com/dancodingbr/personal-blog" rel="noopener noreferrer"&gt;gitlab.com/dancodingbr/personal-blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;&lt;a id="ref-1"&gt;&lt;/a&gt;[1] HashiCorp. &lt;em&gt;Providers — Terraform Language&lt;/em&gt;. HashiCorp Developer.&lt;br&gt;
&lt;a href="https://developer.hashicorp.com/terraform/language/providers" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;a href="https://developer.hashicorp.com/terraform/language/providers" rel="noopener noreferrer"&gt;https://developer.hashicorp.com/terraform/language/providers&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="ref-2"&gt;&lt;/a&gt;[2] HashiCorp. &lt;em&gt;Kubernetes Provider — Terraform Registry&lt;/em&gt;.&lt;br&gt;
&lt;a href="https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;a href="https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs" rel="noopener noreferrer"&gt;https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="ref-3"&gt;&lt;/a&gt;[3] HashiCorp. &lt;em&gt;Manage Kubernetes resources with Terraform&lt;/em&gt;. HashiCorp Developer.&lt;br&gt;
&lt;a href="https://developer.hashicorp.com/terraform/tutorials/kubernetes/kubernetes-provider" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;a href="https://developer.hashicorp.com/terraform/tutorials/kubernetes/kubernetes-provider" rel="noopener noreferrer"&gt;https://developer.hashicorp.com/terraform/tutorials/kubernetes/kubernetes-provider&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="ref-4"&gt;&lt;/a&gt;[4] HashiCorp. &lt;em&gt;hashicorp/helm — Terraform Registry&lt;/em&gt;.&lt;br&gt;
&lt;a href="https://registry.terraform.io/providers/hashicorp/helm/latest" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;a href="https://registry.terraform.io/providers/hashicorp/helm/latest" rel="noopener noreferrer"&gt;https://registry.terraform.io/providers/hashicorp/helm/latest&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="ref-5"&gt;&lt;/a&gt;[5] Helm Project. &lt;em&gt;Using Helm&lt;/em&gt;.&lt;br&gt;
&lt;a href="https://helm.sh/docs/intro/using_helm" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;a href="https://helm.sh/docs/intro/using_helm" rel="noopener noreferrer"&gt;https://helm.sh/docs/intro/using_helm&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>terraform</category>
      <category>helm</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Polyglot Persistence in Microservices: Let the Domain Choose the Database</title>
      <dc:creator>Daniel Cordeiro</dc:creator>
      <pubDate>Sat, 23 May 2026 15:01:34 +0000</pubDate>
      <link>https://dev.to/dancodingbr/polyglot-persistence-in-microservices-let-the-domain-choose-the-database-3o7f</link>
      <guid>https://dev.to/dancodingbr/polyglot-persistence-in-microservices-let-the-domain-choose-the-database-3o7f</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;One of the most consequential decisions in microservices architecture is data storage. Monolithic systems traditionally rely on a single relational database to service all needs — a model that worked well for decades but creates tight coupling, limits scalability, and forces every domain to conform to the same persistence paradigm regardless of whether it is the right fit.&lt;/p&gt;

&lt;p&gt;Modern distributed systems have embraced a concept known as &lt;strong&gt;polyglot persistence&lt;/strong&gt; — the practice of using different data storage technologies within the same system, each chosen to match the access patterns and characteristics of the domain it serves. A &lt;a href="https://github.com/dancodingbr/ecommerce" rel="noopener noreferrer"&gt;MVP e-commerce project&lt;/a&gt; examined in this document demonstrates this pattern in a concrete way: three different databases, each serving a distinct microservice, each chosen deliberately.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Three-Database Architecture
&lt;/h2&gt;

&lt;p&gt;The platform studied here organizes data across three specialized stores:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Database&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Rationale&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Order Service&lt;/td&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;Relational (ACID)&lt;/td&gt;
&lt;td&gt;Transactional consistency, financial data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Product Service&lt;/td&gt;
&lt;td&gt;MongoDB&lt;/td&gt;
&lt;td&gt;Document (NoSQL)&lt;/td&gt;
&lt;td&gt;Flexible schemas, rich catalog data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cart Service&lt;/td&gt;
&lt;td&gt;Redis&lt;/td&gt;
&lt;td&gt;In-memory K/V&lt;/td&gt;
&lt;td&gt;Sub-millisecond speed, ephemeral state&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is the &lt;strong&gt;Database per Service&lt;/strong&gt; pattern [1]. Each service owns its database exclusively — no service reads directly from another's store. This boundary enforces loose coupling and allows each team to evolve the schema independently without risk of cross-service breakage.&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%2Fhbrxsfvs9cqtu7smhe73.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%2Fhbrxsfvs9cqtu7smhe73.png" alt=" " width="800" height="530"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  PostgreSQL for the Order Service: ACID as a Requirement
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;relational database&lt;/strong&gt; organizes data into &lt;strong&gt;tables&lt;/strong&gt; — structured grids where every row is a record and every column is a typed, constrained attribute. Relationships between tables are expressed through &lt;strong&gt;foreign keys&lt;/strong&gt;: a column in one table that references the primary key of another, letting the engine enforce referential integrity automatically. This rigid schema is not a limitation but a deliberate guarantee: every row must conform to the same structure, and the engine validates constraints at write time. The payoff is &lt;strong&gt;ACID&lt;/strong&gt; — the ability to group multiple writes into a single all-or-nothing transaction that either commits fully or rolls back completely, leaving the database in a consistent state regardless of failures.&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%2Fueer2zxxe5p0hlr182gg.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%2Fueer2zxxe5p0hlr182gg.png" alt=" " width="534" height="1260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Order Service persists financial records. An order is not just data — it is a legal artifact, a commitment. This makes ACID guarantees non-negotiable.&lt;/p&gt;

&lt;p&gt;The service uses Spring Data JPA with Flyway for schema migrations. The schema reflects classical relational design: parent &lt;code&gt;orders&lt;/code&gt; table with a child &lt;code&gt;order_items&lt;/code&gt; table linked by a foreign key with &lt;code&gt;ON DELETE CASCADE&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;19&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;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&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;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;order_items&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;19&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;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&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;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;fk_order&lt;/span&gt; &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;orders&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="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&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;OrderService.placeOrder()&lt;/code&gt; method is annotated with &lt;code&gt;@Transactional&lt;/code&gt;. This ensures that if any step in the checkout flow fails — building the item list, calculating the total, persisting the record — the database rolls back to a consistent state. The JPA cascade configuration ensures that saving the parent &lt;code&gt;Order&lt;/code&gt; entity also persists all child &lt;code&gt;OrderItem&lt;/code&gt; entities in a single atomic operation.&lt;/p&gt;

&lt;p&gt;Flyway provides versioned, reproducible migration scripts. On startup the service validates that the running schema matches the expected baseline, preventing "works on my machine" drift between environments [2].&lt;/p&gt;




&lt;h2&gt;
  
  
  MongoDB for the Product Service: Schema Flexibility at Catalog Scale
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;document database&lt;/strong&gt; stores data as self-describing records — typically JSON or BSON objects — where each document can carry a different set of fields. There is no enforced column list; a document simply contains whatever the application writes into it. Documents that represent the same concept live in a &lt;strong&gt;collection&lt;/strong&gt;, but the engine does not require them to be structurally identical. This makes document databases well-suited to domains where the data model is heterogeneous.&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%2Funbto02thcyjsd6ug5pr.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%2Funbto02thcyjsd6ug5pr.png" alt=" " width="800" height="180"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Products have heterogeneous attributes: a laptop has RAM and storage, a t-shirt has size and color, a book has an ISBN and author. Fitting all of these into rigid relational columns requires either complex EAV (Entity-Attribute-Value) schemes or sparse nullable columns — both are maintenance burdens.&lt;/p&gt;

&lt;p&gt;MongoDB's document model stores each product as a self-describing JSON document. When the catalog team needs to add a new attribute category, no schema migration is required. The application code simply begins writing the new field, and existing documents remain valid.&lt;/p&gt;

&lt;p&gt;The Product Service uses Spring Data MongoDB with repository abstraction:&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="nd"&gt;@Document&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;collection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"products"&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;Product&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Id&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;BigDecimal&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Integer&lt;/span&gt; &lt;span class="n"&gt;stockQuantity&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;skuCode&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;category&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;The &lt;code&gt;@Document&lt;/code&gt; annotation maps the Java class to a MongoDB collection. Spring Data's &lt;code&gt;MongoRepository&lt;/code&gt; provides CRUD operations and dynamic query derivation without boilerplate SQL.&lt;/p&gt;




&lt;h2&gt;
  
  
  Redis for the Cart Service: Ephemeral State at Memory Speed
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;key-value store&lt;/strong&gt; is the simplest of all database models: every entry is a pair of a unique &lt;strong&gt;key&lt;/strong&gt; and an associated &lt;strong&gt;value&lt;/strong&gt;, with no enforced structure beyond that. There is no schema, no query language, and no relational machinery — retrieval is always by key, and the engine does nothing more than store and fetch the associated value as fast as possible. That simplicity is what makes key-value stores fast: without the overhead of parsing queries, enforcing constraints, or managing transaction logs, the engine can serve reads and writes at memory speed.&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%2Fd6tujuecmdxcg60xj3dv.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%2Fd6tujuecmdxcg60xj3dv.png" alt=" " width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A shopping cart is session-like: it changes frequently, needs sub-millisecond read/write response times, and is inherently transient — if a cart is lost, the customer can simply re-add items. These characteristics make a relational database an inappropriate choice (too much transactional overhead for short-lived state) and a document database acceptable but not optimal.&lt;/p&gt;

&lt;p&gt;Redis was designed precisely for this use case. As an in-memory data structure store, it delivers microsecond latency for key-value operations [3]. The Cart Service models cart data as a Redis Hash where the top-level key is &lt;code&gt;cart:{userId}&lt;/code&gt; and the value is a JSON-serialized &lt;code&gt;Cart&lt;/code&gt; object.&lt;/p&gt;

&lt;p&gt;The custom &lt;code&gt;RedisConfig&lt;/code&gt; configures a &lt;code&gt;RedisTemplate&lt;/code&gt; with explicit serializers:&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="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;RedisTemplate&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;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;redisTemplate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RedisConnectionFactory&lt;/span&gt; &lt;span class="n"&gt;connectionFactory&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;RedisTemplate&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;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;template&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;RedisTemplate&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;
    &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setConnectionFactory&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connectionFactory&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="nc"&gt;ObjectMapper&lt;/span&gt; &lt;span class="n"&gt;mapper&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;ObjectMapper&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="nc"&gt;JacksonJsonRedisSerializer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;serializer&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;JacksonJsonRedisSerializer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;mapper&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setKeySerializer&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;StringRedisSerializer&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setValueSerializer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serializer&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setHashKeySerializer&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;StringRedisSerializer&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setHashValueSerializer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serializer&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;template&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;This configuration ensures keys are stored as human-readable strings (&lt;code&gt;cart:user123&lt;/code&gt;) while values are stored as JSON, which is both inspectable via Redis CLI and portable across service restarts.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Trade-offs: What This Architecture Costs
&lt;/h2&gt;

&lt;p&gt;Polyglot persistence is not free. The benefits in autonomy and performance come with real operational costs. For example:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No cross-service joins.&lt;/strong&gt; The Order Service cannot join its &lt;code&gt;orders&lt;/code&gt; table directly against MongoDB's &lt;code&gt;products&lt;/code&gt; collection. In a monolith, a JOIN happens inside the database engine in microseconds with full transactional isolation. Across services, the equivalent operation is an HTTP round trip, which introduces variable latency and a dependency on network availability. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Eventual consistency.&lt;/strong&gt; In the &lt;code&gt;OrderService.placeOrder()&lt;/code&gt; method, when a user checks out, the cart is cleared via a &lt;code&gt;try/catch&lt;/code&gt; — a failure there does not roll back the already-committed order. True cross-service transactions require the Saga pattern [4].&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Operational overhead.&lt;/strong&gt; Running PostgreSQL, MongoDB, and Redis alongside RabbitMQ and Keycloak in a single &lt;code&gt;docker-compose.yml&lt;/code&gt; is achievable for development, but each store requires separate backup strategies, monitoring, and operational expertise in production.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




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

&lt;p&gt;The demo e-commerce platform described here demonstrates that polyglot persistence, when applied with intention, produces a system where each component operates at its natural best. PostgreSQL provides the ACID guarantees that financial records demand. MongoDB provides the flexibility that a diverse product catalog requires. Redis provides the speed that shopping cart interactions need.&lt;/p&gt;

&lt;p&gt;The key insight is that the choice of persistence technology should follow from the domain's requirements — not from organizational familiarity or the path of least resistance. In practice, this means asking a different set of questions before reaching for a database. Is the data relational, or is it a self-describing document with variable structure? Is consistency a hard requirement, or can the system tolerate brief divergence in exchange for availability and speed? Is the data long-lived and auditable, or ephemeral by nature? Each of these questions points toward a different storage paradigm, and no single engine answers all of them optimally. &lt;/p&gt;




&lt;p&gt;&lt;em&gt;Source code: &lt;a href="https://github.com/dancodingbr/ecommerce" rel="noopener noreferrer"&gt;github.com/dancodingbr/ecommerce&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;&lt;a id="ref-1"&gt;&lt;/a&gt;[1] Microservices.io. &lt;em&gt;Pattern: Database per service&lt;/em&gt;. Available at: &lt;a href="https://microservices.io/patterns/data/database-per-service.html" rel="noopener noreferrer"&gt;https://microservices.io/patterns/data/database-per-service.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="ref-2"&gt;&lt;/a&gt;[2] Flyway. &lt;em&gt;Why database migrations&lt;/em&gt;. Available at: &lt;a href="https://documentation.red-gate.com/fd/why-database-migrations-184127574.html" rel="noopener noreferrer"&gt;https://documentation.red-gate.com/fd/why-database-migrations-184127574.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="ref-3"&gt;&lt;/a&gt;[3] Redis. &lt;em&gt;Get Started&lt;/em&gt;. Available at: &lt;a href="https://redis.io/docs/latest/get-started/" rel="noopener noreferrer"&gt;https://redis.io/docs/latest/get-started/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="ref-4"&gt;&lt;/a&gt;[4] Microservices.io. &lt;em&gt;Pattern: Saga&lt;/em&gt;.  Available at: &lt;a href="https://microservices.io/patterns/data/saga.html" rel="noopener noreferrer"&gt;https://microservices.io/patterns/data/saga.html&lt;/a&gt;&lt;/p&gt;

</description>
      <category>microservices</category>
      <category>database</category>
      <category>springboot</category>
      <category>java</category>
    </item>
  </channel>
</rss>
