<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged eventdriven</title>
<link>https://coderlegion.com/tag/eventdriven</link>
<description>Powered by Question2Answer</description>
<item>
<title>Pub/Sub Isn't Event-Driven Architecture: Here's What Most Teams Miss</title>
<link>https://coderlegion.com/32197/pub-sub-isnt-event-driven-architecture-heres-what-most-teams-miss</link>
<description>&lt;p&gt;Pub/Sub is a messaging pattern. Event-Driven Architecture is an architectural discipline. Every EDA implementation may use Pub/Sub, but not every Pub/Sub implementation is truly Event-Driven Architecture.&lt;/p&gt;
&lt;p&gt;For many years, I would have agreed with the statement that Pub/Sub equals Event-Driven Architecture (EDA).&lt;/p&gt;
&lt;p&gt;Why? Because early discussions around EDA focused primarily on three characteristics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Real-time communication &lt;/li&gt;
&lt;li&gt;Publish/Subscribe messaging &lt;/li&gt;
&lt;li&gt;Asynchronous processing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At that time, implementing a Pub/Sub broker was often considered synonymous with implementing EDA.&lt;/p&gt;
&lt;p&gt;Today, however, the answer is no.&lt;/p&gt;
&lt;p&gt;As organizations have adopted EDA at scale, the discipline has evolved from a messaging concept into a comprehensive architectural practice. Modern EDA encompasses far more than simply publishing and subscribing to messages.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;📡EDA Is More Than Messaging&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A mature Event-Driven Architecture consists of multiple foundational elements:&lt;br&gt;
&lt;strong&gt;📝Event Definition&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Events must be clearly defined and categorized.&lt;/p&gt;
&lt;p&gt;Examples include:&lt;br&gt;
Business Events: CustomerRegistered, OrderPlaced, PaymentCompleted&lt;br&gt;
System Events: FileUploaded, ServiceStarted, DatabaseUpdated, EOD&lt;/p&gt;
&lt;p&gt;Without a consistent event model, organizations quickly end up with thousands of loosely defined messages that are difficult to understand and maintain.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;👤Event Ownership&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Just like data, events are business assets.&lt;/p&gt;
&lt;p&gt;Every event should have a clearly identified owner responsible for:&lt;br&gt;
Event definition&lt;br&gt;
Schema management&lt;br&gt;
Versioning&lt;br&gt;
Lifecycle governance&lt;/p&gt;
&lt;p&gt;When nobody owns an event, everyone assumes somebody else does.&lt;br&gt;
&lt;strong&gt;📏Event and Queue Standards&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Topic (event) and queue naming conventions form the foundation of a scalable EDA platform.&lt;br&gt;
A well-defined naming structure enables:&lt;br&gt;
Event discoverability&lt;br&gt;
Consistent routing&lt;br&gt;
Easier troubleshooting&lt;br&gt;
Better governance&lt;br&gt;
Without standards, the event landscape quickly becomes chaotic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;🔀Event Flow Design&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;EDA is not merely about sending messages.&lt;/p&gt;
&lt;p&gt;It is about how events collaborate to achieve business outcomes.&lt;/p&gt;
&lt;p&gt;Modern architectures require a clear understanding of:&lt;br&gt;
Event relationships&lt;br&gt;
Event choreography&lt;br&gt;
Event dependencies&lt;br&gt;
Business process visibility&lt;/p&gt;
&lt;p&gt;While services remain loosely coupled, the overall event flow must still be intentionally designed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;🛡️Event Governance&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Governance is the difference between a sustainable platform and an uncontrolled messaging ecosystem.&lt;/p&gt;
&lt;p&gt;Effective governance covers:&lt;br&gt;
Schema management&lt;br&gt;
Data classification&lt;br&gt;
Security controls&lt;br&gt;
Access management&lt;br&gt;
Event lifecycle management&lt;br&gt;
Compliance and auditing&lt;/p&gt;
&lt;p&gt;Governance should be embedded throughout the entire lifecycle, from design through implementation and operations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;📬Pub/Sub Is Only One Pattern Within EDA&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Pub/Sub is an important communication pattern, but it is only one building block of a broader architecture.&lt;br&gt;
A Pub/Sub platform without governance, ownership, standards, and event design is simply a collection of disconnected messages.&lt;/p&gt;
&lt;p&gt;In such environments, organizations commonly encounter problems such as:&lt;br&gt;
&lt;strong&gt;⚠️Consumer Failures&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Consumers may crash when:&lt;br&gt;
Unexpected event types are received&lt;br&gt;
Event schemas change without coordination&lt;br&gt;
Required fields are removed or modified&lt;br&gt;
Data Exposure Risks&lt;/p&gt;
&lt;p&gt;Sensitive information, including Personally Identifiable Information (PII), may be unintentionally delivered to systems or teams that should not have access to it.&lt;br&gt;
Message Loss or Misrouting&lt;/p&gt;
&lt;p&gt;Poorly governed subscriptions can result in:&lt;br&gt;
Events being consumed by unintended systems&lt;br&gt;
Important messages being ignored&lt;br&gt;
Business processes becoming unreliable&lt;br&gt;
Rising Infrastructure Costs&lt;/p&gt;
&lt;p&gt;Unnecessary event distribution increases:&lt;br&gt;
Network traffic&lt;br&gt;
Broker workload&lt;br&gt;
Infrastructure consumption&lt;br&gt;
Operational costs&lt;br&gt;
What begins as architectural flexibility can quickly become architectural sprawl.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;💡 Final Thoughts&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Many organizations begin their event-driven journey by deploying a message broker and enabling Pub/Sub communication. While this is an important first step, it is only the foundation.&lt;br&gt;
The real challenge is not moving events between systems. The challenge is managing events as long-lived business assets.&lt;/p&gt;
&lt;p&gt;Without clear ownership, governance, standards, and lifecycle management, organizations often end up with an event ecosystem that is difficult to understand, secure, and evolve. Teams gain asynchronous communication, but lose visibility, consistency, and control.&lt;/p&gt;
&lt;p&gt;Successful Event-Driven Architecture is not measured by how many events are produced each day. It is measured by how effectively those events enable business capabilities while remaining discoverable, governed, and trusted across the enterprise.&lt;/p&gt;
&lt;p&gt;In other words:&lt;br&gt;
A message broker can make your systems event-enabled.&lt;br&gt;
Governance, ownership, and standards make your organization truly event-driven.&lt;/p&gt;
&lt;p&gt;❓ What do you think? Have you seen organizations mistake Pub/Sub implementation for Event-Driven Architecture? What governance challenges emerged as event adoption grew?&lt;/p&gt;
&lt;p&gt;This typically drives significantly more comments and discussion from architects, developers, and integration professionals.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32197/pub-sub-isnt-event-driven-architecture-heres-what-most-teams-miss</guid>
<pubDate>Fri, 23 Oct 2026 14:38:21 +0000</pubDate>
</item>
<item>
<title>🚦 EDA Without Governance: A Fast Track to Enterprise Chaos</title>
<link>https://coderlegion.com/31224/eda-without-governance-a-fast-track-to-enterprise-chaos</link>
<description>&lt;p&gt;In today's digital enterprise, Event-Driven Architecture (EDA) has become a preferred architectural pattern for building scalable, loosely coupled, and real-time systems. By enabling applications to publish and subscribe to events independently, EDA provides the flexibility and agility required to support modern business demands.&lt;/p&gt;
&lt;p&gt;However, there is a critical truth that many organizations discover only after their event ecosystem starts to grow:&lt;/p&gt;
&lt;p&gt;⚠️ The success of EDA depends less on technology and more on governance.&lt;/p&gt;
&lt;p&gt;Without proper governance, an event bus can quickly evolve into a chaotic integration landscape where dependencies are unclear, ownership becomes fragmented, and changes introduce widespread downstream impacts.&lt;/p&gt;
&lt;p&gt;🌐 EDA Is More Than Event Distribution&lt;/p&gt;
&lt;p&gt;EDA is fundamentally a distribution design that allows applications to publish and consume events across the enterprise.&lt;/p&gt;
&lt;p&gt;✅ New publishers can share information with multiple consumers.&lt;br&gt;
✅ New consumers can subscribe to existing event streams.&lt;br&gt;
✅ Applications remain loosely coupled and independently deployable.&lt;/p&gt;
&lt;p&gt;But this flexibility comes with responsibility.&lt;/p&gt;
&lt;p&gt;An event published today may be consumed by numerous applications, either directly or indirectly. As adoption grows, every design decision becomes part of a shared enterprise contract.&lt;/p&gt;
&lt;p&gt;Without governance, what starts as flexibility can quickly become complexity.&lt;/p&gt;
&lt;p&gt;⚠️ The Hidden Risk of Uncontrolled Growth&lt;/p&gt;
&lt;p&gt;When onboarding new applications into an event-driven ecosystem, governance standards and processes should be established before implementation begins.&lt;/p&gt;
&lt;p&gt;Without these controls:&lt;/p&gt;
&lt;p&gt;🔴 Topic structures become inconsistent.&lt;br&gt;
🔴 Applications subscribe to unnecessary events.&lt;br&gt;
🔴 Event routing becomes difficult to understand and maintain.&lt;br&gt;
🔴 Sensitive data may be exposed to unauthorized consumers.&lt;br&gt;
🔴 Small changes trigger large-scale application rework.&lt;/p&gt;
&lt;p&gt;The impact is not limited to event broker configurations.&lt;/p&gt;
&lt;p&gt;A seemingly simple change to an event topic, payload structure, or routing pattern may require modifications across multiple consuming applications, testing cycles, operational procedures, and business processes.&lt;/p&gt;
&lt;p&gt;In large-scale enterprises, a single event redesign can affect dozens of interconnected systems.&lt;/p&gt;
&lt;p&gt;🔄 Governance Must Cover the Entire Lifecycle&lt;/p&gt;
&lt;p&gt;EDA governance is not a one-time architecture review.&lt;/p&gt;
&lt;p&gt;It should span the entire delivery lifecycle:&lt;/p&gt;
&lt;p&gt;📋 Design&lt;br&gt;
👨💻 Development&lt;br&gt;
🧪 Testing&lt;br&gt;
🚀 Deployment&lt;br&gt;
⚙️ Production Operations&lt;/p&gt;
&lt;p&gt;Any governance gap can result in:&lt;/p&gt;
&lt;p&gt;❌ Increased technical debt&lt;br&gt;
❌ Integration failures&lt;br&gt;
❌ Security and compliance risks&lt;br&gt;
❌ Operational complexity&lt;br&gt;
❌ Higher maintenance costs&lt;/p&gt;
&lt;p&gt;The most successful event-driven organizations balance architectural flexibility with disciplined governance.&lt;/p&gt;
&lt;p&gt;🎯 Key Areas of EDA Governance&lt;/p&gt;
&lt;p&gt;🏷️ 1. Topic Naming Standards&lt;/p&gt;
&lt;p&gt;Topics are the foundation of event discoverability and maintainability.&lt;/p&gt;
&lt;p&gt;Every topic should follow a mandatory naming standard that clearly identifies:&lt;/p&gt;
&lt;p&gt;✅ Business domain&lt;br&gt;
✅ Business capability&lt;br&gt;
✅ Event type&lt;br&gt;
✅ Environment&lt;/p&gt;
&lt;p&gt;A consistent naming convention helps architects, developers, and support teams quickly understand event purpose and ownership.&lt;/p&gt;
&lt;p&gt;Good governance starts with naming things correctly.&lt;/p&gt;
&lt;p&gt;📬 2. Queue Subscription Governance&lt;/p&gt;
&lt;p&gt;Applications should subscribe only to events that are required for their business functions.&lt;/p&gt;
&lt;p&gt;Governance should ensure:&lt;/p&gt;
&lt;p&gt;✅ Controlled subscription approval&lt;br&gt;
✅ Consumer registration and ownership&lt;br&gt;
✅ Dependency visibility&lt;br&gt;
✅ Periodic subscription reviews&lt;/p&gt;
&lt;p&gt;This prevents unnecessary event consumption and reduces future change impacts.&lt;/p&gt;
&lt;p&gt;Not every application needs every event.&lt;/p&gt;
&lt;p&gt;🔀 3. Event Flow Design&lt;/p&gt;
&lt;p&gt;A well-designed event flow enables systems to evolve without disrupting the broader ecosystem.&lt;/p&gt;
&lt;p&gt;Governance should enforce:&lt;/p&gt;
&lt;p&gt;✅ Standard routing principles&lt;br&gt;
✅ Clear producer and consumer responsibilities&lt;br&gt;
✅ Traceable event paths&lt;br&gt;
✅ Flexible addition and removal of components&lt;/p&gt;
&lt;p&gt;A properly governed event flow allows organizations to grow their architecture while maintaining control and predictability.&lt;/p&gt;
&lt;p&gt;Flexibility should be intentional, not accidental.&lt;/p&gt;
&lt;p&gt;🔐 4. Data Governance and Access Control&lt;/p&gt;
&lt;p&gt;Not every consumer should have access to every event.&lt;/p&gt;
&lt;p&gt;Organizations must ensure applications can subscribe only to authorized events, especially when they contain:&lt;/p&gt;
&lt;p&gt;🔒 Personally Identifiable Information (PII)&lt;br&gt;
🔒 Customer information&lt;br&gt;
🔒 Financial transactions&lt;br&gt;
🔒 Business-sensitive data&lt;/p&gt;
&lt;p&gt;Strong governance ensures:&lt;/p&gt;
&lt;p&gt;✅ Data privacy&lt;br&gt;
✅ Regulatory compliance&lt;br&gt;
✅ Security controls&lt;br&gt;
✅ Consumer authorization&lt;/p&gt;
&lt;p&gt;The principle should be simple:&lt;/p&gt;
&lt;p&gt;🔑 Subscribe only to the data you are authorized to consume.&lt;/p&gt;
&lt;p&gt;🏆 Governance Determines EDA Success&lt;/p&gt;
&lt;p&gt;Many organizations focus heavily on event brokers, event meshes, streaming platforms, and messaging technologies.&lt;/p&gt;
&lt;p&gt;While these technologies are important, they are not what ultimately determines the success of an Event-Driven Architecture.&lt;/p&gt;
&lt;p&gt;The true differentiator is governance.&lt;/p&gt;
&lt;p&gt;Well-defined standards, ownership models, review processes, security controls, and lifecycle management enable organizations to scale their event ecosystem confidently and sustainably.&lt;/p&gt;
&lt;p&gt;Without governance, flexibility becomes chaos.&lt;/p&gt;
&lt;p&gt;With governance, EDA becomes a strategic enabler of enterprise agility, innovation, and business growth.&lt;/p&gt;
&lt;p&gt;💡 Final Thought&lt;/p&gt;
&lt;p&gt;🚦 EDA without governance is simply message transportation.&lt;/p&gt;
&lt;p&gt;🚀 EDA with governance becomes sustainable enterprise architecture.&lt;/p&gt;
&lt;p&gt;The question is not whether your organization should adopt Event-Driven Architecture.&lt;/p&gt;
&lt;p&gt;The real question is:&lt;/p&gt;
&lt;p&gt;Do you have the governance needed to keep it from becoming enterprise chaos?&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31224/eda-without-governance-a-fast-track-to-enterprise-chaos</guid>
<pubDate>Thu, 15 Oct 2026 16:03:25 +0000</pubDate>
</item>
<item>
<title>Stop Treating EDA as Just Pub/Sub. Here’s How to Handle Request/Reply</title>
<link>https://coderlegion.com/30621/stop-treating-eda-as-just-pub-sub-heres-how-to-handle-request-reply</link>
<description>&lt;h2 id=&quot;-eda-just-pub-sub-building-flexible-request-reply-journeys-through-event-choreography&quot;&gt;⚡ EDA ≠ Just Pub/Sub: Building Flexible Request/Reply Journeys Through Event Choreography&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;🚀 &lt;strong&gt;Exciting Update:&lt;/strong&gt; Incredibly honored that this deep dive was selected for the &lt;strong&gt;HackerNoon Science Tech Brief&lt;/strong&gt; within 12 hours of its original release! Sharing it here with the Coder Legion community to hear your thoughts on EDA patterns.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;Note: The complete, ready-to-use article template covering the core dilemma of synchronous request/reply flows, the multi-payment processing blueprint with topic naming standards, zero-routing choreography principles, and engagement prompts for the community is fully prepared for your publication.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;⚡ The Hidden Power of EDA: Building Flexible Request/Reply Journeys Through Event Choreography&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When engineers discuss Event-Driven Architecture (EDA), the conversation typically centers on asynchronous pub/sub patterns, fire-and-forget processing, and real-time streaming. We praise EDA for decoupling our systems, but we often hit a wall when a core business process inherently demands a synchronous request-reply interaction.&lt;/p&gt;
&lt;p&gt;A common misconception is that EDA is poorly suited for request-reply workflows. The standard instinct is to fall back on direct, synchronous HTTP/gRPC service calls between microservices. However, this introduces tight coupling, making it difficult to scale, evolve, or independently deploy components.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;🎯 The Challenge&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In traditional request/reply designs, systems can become tightly coupled through direct service calls, making it harder to scale, evolve, or introduce new capabilities into the workflow.&lt;/p&gt;
&lt;p&gt;A well-designed event-driven choreography offers an alternative approach.&lt;/p&gt;
&lt;p&gt;It enables an end-to-end business process to flow seamlessly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Client Request&lt;/li&gt;
&lt;li&gt;Backend Services&lt;/li&gt;
&lt;li&gt;Multiple Processing Components&lt;/li&gt;
&lt;li&gt;Business Decisions&lt;/li&gt;
&lt;li&gt;Final Response to Client&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Regardless of how many systems participate in the journey.&lt;/p&gt;
&lt;p&gt;As a result, EDA becomes more than a mechanism for event distribution. It becomes a powerful foundation for building scalable and flexible request/reply workflows.&lt;/p&gt;
&lt;p&gt;💳 Example: Multi-Payment Processing Platform&lt;/p&gt;
&lt;p&gt;Let's use a payment platform that supports multiple payment methods as an example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bank Transfer&lt;/li&gt;
&lt;li&gt;FPS&lt;/li&gt;
&lt;li&gt;PPS&lt;/li&gt;
&lt;li&gt;EFT&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Although these payment methods follow different processing paths, they can share the same event-driven framework.&lt;br&gt;
&lt;strong&gt;📌 Step 1: Establish a Well-Defined Topic Naming Standard&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The well-defined topic structure with mandatory fields and optional fields. The mandatory part should include at least four fields: information domain, information subdomain, message type, and schema version.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=15090109762337142705&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;📌 Step 2: Use the Naming Standard as the Event Flow Guideline&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Events may travel through many systems during their lifecycle.&lt;/li&gt;
&lt;li&gt;Although different systems may interpret or process the event&lt;br&gt;
differently, the original business context must remain intact. &lt;/li&gt;
&lt;li&gt;The topic representing the event should remain consistent with the&lt;br&gt;
event's original business purpose.&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=72657400590844178&quot; alt=&quot;&quot;&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=1121929266061598601&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;On each event flow, the mandatory parts (red and blue colors) are placed in the message header and brought forward to all subsequent messages in the flow. This part should not be changed throughout the lifetime of the event flow. For each component, the output topic will be the input messages’ mandatory part plus this component function, plus the process status. It is very easy to subscribe to all corresponding events within the same event flow by using a wildcard.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;🧩 Loosely Coupled Processing Components&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;An important characteristic of this design is that individual processing components do not contain built-in routing logic.&lt;/p&gt;
&lt;p&gt;Each component simply:&lt;/p&gt;
&lt;p&gt;📥 Consumes events it is interested in &lt;br&gt;
⚙️ Performs its business function &lt;br&gt;
📤 Publishes the next event&lt;/p&gt;
&lt;p&gt;The component has no knowledge of:&lt;/p&gt;
&lt;p&gt;❌ Which flow it belongs to &lt;br&gt;
❌ Which system produced the event &lt;br&gt;
❌ Which system will consume the next event&lt;/p&gt;
&lt;p&gt;This significantly improves reusability and maintainability.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=5615199361192018044&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;✅ The Result&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This architecture enables a set of common reusable components to support multiple request/reply workflows across different channels and payment methods.&lt;/p&gt;
&lt;p&gt;Key Outcomes&lt;/p&gt;
&lt;p&gt;Consistent event schema across multiple use cases&lt;br&gt;
All downstream events inherit and extend the original business context&lt;br&gt;
Additional analytical services can be introduced without impacting existing applications&lt;br&gt;
Customer behavior and application activities can be monitored in real time&lt;br&gt;
New processing components can be added or removed easily&lt;br&gt;
Existing applications require no code changes&lt;br&gt;
Most changes can be achieved through topic or queue subscription configuration alone&lt;/p&gt;
&lt;p&gt;💡 Final Thought&lt;/p&gt;
&lt;p&gt;One of the biggest misconceptions about Event-Driven Architecture is that it is only suitable for asynchronous event broadcasting.&lt;/p&gt;
&lt;p&gt;In reality, when combined with strong event governance, consistent topic standards, and event choreography, EDA can also provide an elegant solution for complex request/reply interactions.&lt;/p&gt;
&lt;p&gt;The result is an architecture that is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Loosely Coupled &lt;/li&gt;
&lt;li&gt;Highly Scalable &lt;/li&gt;
&lt;li&gt;Easily Extensible &lt;/li&gt;
&lt;li&gt;Business-Driven &lt;/li&gt;
&lt;li&gt;Future-Proof&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;How does your organization handle request/reply patterns in an event-driven landscape? I'd love to hear your experiences and perspectives in the comments.&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30621/stop-treating-eda-as-just-pub-sub-heres-how-to-handle-request-reply</guid>
<pubDate>Tue, 13 Oct 2026 05:13:00 +0000</pubDate>
</item>
<item>
<title>Asistente Zyro</title>
<link>https://coderlegion.com/30662/asistente-zyro</link>
<description>&lt;h4 id=&quot;-qu-es-asistente-zyro-&quot;&gt;¿Qué es Asistente Zyro?&lt;/h4&gt;
&lt;p&gt;Asistente Zyro es un agente autónomo de IA diseñado para la automatización de procesos B2B. A diferencia de los chatbots convencionales, Zyro actúa como un operador multicanal asíncrono capaz de crear automatizaciones recurrentes e interactuar con el mundo físico (telefonía) mediante infraestructura &lt;em&gt;Event-Driven&lt;/em&gt;.&lt;/p&gt;
&lt;h4 id=&quot;caracter-sticas-y-capacidades-clave&quot;&gt;Características y Capacidades Clave&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Creación de Automatizaciones en Tiempo Real:&lt;/strong&gt; Puedes pedirle a Zyro que clasifique documentos en tu Google Drive. El agente interactúa contigo mediante lenguaje natural y botones dinámicos en el chat para crear reglas recurrentes (por ejemplo, analizar correos entrantes diariamente y guardar adjuntos específicos en carpetas auto-creadas). &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Integración Telefónica Real (PBX) y Negociación:&lt;/strong&gt; Zyro no solo envía correos. Puede realizar llamadas asíncronas a clientes para dar seguimiento a un documento, negociar horarios de reunión por voz y, tras colgar, reportar el nuevo estado y agendar la cita en tu calendario.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Orquestación Multiusuario por Correo:&lt;/strong&gt; Al poner a &lt;code&gt;*Emails are not allowed*&lt;/code&gt; en copia en un hilo de correo, el agente analiza los calendarios de los involucrados, encuentra huecos comunes y propone la fecha ideal, liberando a tu equipo del caos administrativo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flujos de Trabajo de Extremo a Extremo:&lt;/strong&gt; En un solo &lt;em&gt;prompt&lt;/em&gt;, Zyro puede: generar un borrador, convertirlo en Google Doc, enviarlo por Gmail, llamar al destinatario y actualizar tu Google Calendar.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Seguridad Zero-Retention:&lt;/strong&gt; La IA nunca almacena ni posee tus datos. Toda la ejecución de código (lectura de correos, modificación de Drive) se realiza inyectando tokens OAuth2 nativos de Google Workspace mediante middleware, garantizando seguridad de nivel corporativo.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;iframe width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube.com/embed/bbtT0-1bVDs?si=mOfPqfsDfN_q91UN&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;allowfullscreen&quot;&gt;&lt;/iframe&gt;&lt;/p&gt;
&lt;h4 id=&quot;-para-qui-n-es-el-software-&quot;&gt;¿Para quién es el software?&lt;/h4&gt;
&lt;p&gt;Zyro está diseñado para empresas, agencias y consultorías que necesitan escalar su productividad sin aumentar su carga administrativa. Es la solución para directivos que buscan delegar las tareas de seguimiento (llamadas, agendamiento de reuniones, clasificación documental) a un sistema predecible, seguro y sin curva de aprendizaje.&lt;/p&gt;
</description>
<category>Launches</category>
<guid isPermaLink="true">https://coderlegion.com/30662/asistente-zyro</guid>
<pubDate>Sun, 11 Oct 2026 00:34:39 +0000</pubDate>
</item>
<item>
<title>📊 Can Kafka Handle Every Integration Use Case? Understanding the Architectural Trade-offs</title>
<link>https://coderlegion.com/30143/can-kafka-handle-every-integration-use-case-understanding-the-architectural-trade-offs</link>
<description>&lt;p&gt;Compared with traditional queue-based messaging platforms, Kafka is primarily designed for event streaming. It excels at distributing data from producers to one or many consumers at massive scale. However, unlike many traditional messaging platforms, Kafka intentionally minimizes broker-side routing and content-based filtering capabilities. Message selection is typically achieved through topic design, partitioning strategies, stream processing, or consumer-side filtering rather than broker-managed routing.&lt;/p&gt;
&lt;p&gt;This raises an important architectural question:&lt;/p&gt;
&lt;p&gt;❓ Can Kafka be used for all integration and messaging use cases?&lt;/p&gt;
&lt;p&gt;The answer is yes, but organizations should understand and address several architectural and operational challenges before adopting Kafka as a universal messaging platform.&lt;/p&gt;
&lt;p&gt; 📈 1. Scaling Considerations&lt;/p&gt;
&lt;p&gt;Kafka's distributed architecture allows horizontal scaling by adding additional brokers to support increasing workloads and client connections. However, consumer scalability is fundamentally tied to the number of partitions within a topic.&lt;/p&gt;
&lt;p&gt;Key considerations:&lt;/p&gt;
&lt;p&gt;✅ One partition can be consumed by only one consumer within the same consumer group.&lt;br&gt;
✅ One consumer can consume multiple partitions.&lt;/p&gt;
&lt;p&gt; ⚠️ The maximum parallelism of a consumer group is limited by the partition count.&lt;/p&gt;
&lt;p&gt;As a result, partitioning strategy becomes a critical design decision. Under-partitioning can limit future scalability, while over-partitioning may introduce unnecessary operational overhead, metadata overhead, replication traffic, and recovery times, making partition planning a long-term architectural decision.&lt;/p&gt;
&lt;p&gt;Partition planning should be treated as a long-term architectural decision, with future scalability requirements carefully considered during initial design.&lt;/p&gt;
&lt;p&gt;🛡️ 2. Reliability and Data Durability&lt;/p&gt;
&lt;p&gt;Kafka provides flexible durability options through replication and acknowledgment settings.&lt;/p&gt;
&lt;p&gt;Publishers can choose different acknowledgment modes, balancing performance against durability requirements.&lt;/p&gt;
&lt;p&gt;This flexibility offers advantages, but it also introduces governance challenges:&lt;/p&gt;
&lt;p&gt;⚠️ Different applications may adopt different reliability settings.&lt;br&gt;
⚠️ Organizations need governance and standards to ensure mission-critical applications meet reliability requirements.&lt;/p&gt;
&lt;p&gt;In addition, replication significantly reduces the risk of data loss, but durability guarantees ultimately depend on replication factors, acknowledgment settings, in-sync replica requirements, and storage configuration. Kafka's design prioritizes minimizing the probability of data loss while maintaining high throughput. Kafka achieves durable storage through persistent logs and replication. While Kafka leverages the operating system page cache to maximize throughput, durability ultimately depends on replication, acknowledgment settings, storage configuration, and broker availability. These mechanisms provide strong durability characteristics, although architects should carefully evaluate configurations based on business requirements.&lt;/p&gt;
&lt;p&gt;Architects should therefore carefully assess durability requirements and select appropriate replication, acknowledgment, and storage configurations.&lt;/p&gt;
&lt;p&gt; 🔄 3. Consumer Group Rebalancing&lt;/p&gt;
&lt;p&gt;Consumer groups provide scalability and high availability, but they come with an operational cost.&lt;/p&gt;
&lt;p&gt;Whenever a consumer joins, leaves, fails, or recovers within a consumer group, Kafka may initiate a rebalance process.&lt;/p&gt;
&lt;p&gt;During rebalancing:&lt;/p&gt;
&lt;p&gt;Partition ownership is reassigned&lt;br&gt;
Consumer-to-partition mappings are recalculated.&lt;br&gt;
Message consumption may be temporarily paused.&lt;/p&gt;
&lt;p&gt;For dynamic cloud-native applications that frequently scale in and out, excessive consumer group rebalancing can disrupt message processing and increase latency. By default, Kafka does not guarantee partition ownership stability during rebalances. To minimize disruption, consider using the Cooperative Sticky Assignor or Static Membership.&lt;/p&gt;
&lt;p&gt;⚙️ 4. Group Coordinator Bottlenecks&lt;/p&gt;
&lt;p&gt;Kafka's data plane is highly scalable and typically handles large volumes of producers and consumers efficiently.&lt;/p&gt;
&lt;p&gt;However, consumer groups rely on a control-plane component known as the Group Coordinator.&lt;/p&gt;
&lt;p&gt;The Group Coordinator is responsible for:&lt;/p&gt;
&lt;p&gt;📌 Managing group membership&lt;br&gt;
📌 Handling consumer rebalancing&lt;br&gt;
📌 Processing heartbeats&lt;br&gt;
📌 Managing offset commits and retrievals&lt;br&gt;
📌 Enforcing group protocols and coordination logic&lt;/p&gt;
&lt;p&gt;In very large deployments with many consumer groups and frequent rebalances, Group Coordinator load can become an important operational consideration. When many consumer groups rebalance simultaneously, Group Coordinator load may increase significantly, potentially leading to longer rebalance times and higher consumer recovery latency.&lt;/p&gt;
&lt;p&gt;🔀 5. Message Distribution Models&lt;/p&gt;
&lt;p&gt;Traditional message brokers and Kafka use fundamentally different message distribution approaches.&lt;/p&gt;
&lt;p&gt;Message Bus Model&lt;/p&gt;
&lt;p&gt;✅ Multiple consumers can subscribe to the same non-exclusive queue.&lt;br&gt;
✅ The broker distributes messages among available consumers.&lt;br&gt;
✅ Consumer scaling can be automated using tools such as Kubernetes Event-Driven Autoscaling (KEDA) based on queue depth.&lt;/p&gt;
&lt;p&gt; Kafka Model&lt;/p&gt;
&lt;p&gt;✅ Messages are distributed across partitions.&lt;br&gt;
✅ Each partition is processed by only one consumer within a consumer group.&lt;br&gt;
✅ Scaling is achieved through adding partitions and consumers.&lt;/p&gt;
&lt;p&gt;However:&lt;/p&gt;
&lt;p&gt;⚠️ Message distribution occurs at the partition level.&lt;br&gt;
⚠️ New messages continue to be sent to assigned partitions regardless of individual consumer performance.&lt;/p&gt;
&lt;p&gt;If one consumer becomes significantly slower than others, backlog accumulation can occur on specific partitions, resulting in uneven processing rates across the consumer group.&lt;/p&gt;
&lt;p&gt;Kafka has introduced the Share Group model (KIP-932), which aims to provide queue-like work-distribution semantics similar to traditional non-exclusive queues. However, the feature is still evolving and has not yet reached the same maturity level as established enterprise messaging platforms.&lt;/p&gt;
&lt;p&gt;🌐 6. Network Bandwidth Consumption&lt;/p&gt;
&lt;p&gt;One of the key differences between Kafka and many traditional message buses is the absence of native broker-side routing and filtering.&lt;/p&gt;
&lt;p&gt;In many messaging platforms, consumers receive only messages that match specific routing rules.&lt;/p&gt;
&lt;p&gt;With Kafka:&lt;/p&gt;
&lt;p&gt;📥 Consumers typically subscribe to topics.&lt;br&gt;
📥 Kafka favors topic-based distribution. As consumer requirements diverge, additional topics, stream processing, or filtering may be needed, increasing operational overhead.&lt;br&gt;
📥 When consumer requirements diverge significantly, organizations may need additional topics, stream processing, or consumer-side filtering, potentially increasing operational complexity and network traffic.&lt;/p&gt;
&lt;p&gt;For environments with large event volumes and diverse consumer requirements, this approach may increase:&lt;/p&gt;
&lt;p&gt;📡 Network bandwidth usage&lt;br&gt;
🖥️ Consumer processing overhead&lt;/p&gt;
&lt;p&gt;Increasing the number of Kafka topics is one way to address bandwidth and scalability challenges. However, this approach can significantly increase operational overhead and application complexity. Architects should carefully evaluate topic design and event segmentation strategies to minimize unnecessary data movement and avoid excessive topic proliferation.&lt;/p&gt;
&lt;p&gt;📋7. Message Ordering vs Scalability&lt;/p&gt;
&lt;p&gt;One of Kafka's key strengths is its ability to preserve message order, but this comes with an important architectural trade-off.&lt;/p&gt;
&lt;p&gt;Kafka guarantees message ordering only within a partition.&lt;/p&gt;
&lt;p&gt;To increase processing throughput and consumer parallelism, organizations often increase the number of partitions. However, as partitions increase, maintaining a single global order across all messages becomes more difficult.&lt;/p&gt;
&lt;p&gt;📦 8. Topic and Metadata Management&lt;/p&gt;
&lt;p&gt;In very large Kafka deployments, metadata size and frequent metadata updates can increase client startup time, subscription management overhead, and cluster coordination activity. While Kafka is designed to scale efficiently, excessive topic and partition growth can create additional operational and management challenges.&lt;/p&gt;
&lt;p&gt;🎯 Final Thoughts&lt;/p&gt;
&lt;p&gt;Kafka is an exceptional event-streaming platform and has become a popular foundation for modern event-driven architecture. However, &quot;one platform for every use case&quot; is not always the optimal architectural approach.&lt;/p&gt;
&lt;p&gt;The key question is not whether Kafka can support a use case, but whether it is the most appropriate tool for that use case.&lt;/p&gt;
&lt;p&gt;Successful architecture decisions require balancing:&lt;/p&gt;
&lt;p&gt;📈 Scalability&lt;br&gt;
🛡️ Reliability&lt;br&gt;
⚙️ Operational Complexity&lt;br&gt;
🔄 Consumer Distribution Requirements&lt;br&gt;
🌐 Network Efficiency&lt;/p&gt;
&lt;p&gt;High-scale Kafka environments require strong operational observability. Consumer lag, partition imbalance, replication health, broker utilization, and rebalancing behavior must be continuously monitored to ensure reliable service operation.&lt;/p&gt;
&lt;p&gt;However, Kafka is intentionally designed with relatively simple architecture to deliver high-performance event streaming at scale. As a result, it does not provide the rich routing and message filtering capabilities commonly found in traditional message buses.&lt;/p&gt;
&lt;p&gt;Understanding these trade-offs enables architects to select the right messaging paradigm for the right business problem rather than forcing every workload into a single technology pattern.&lt;/p&gt;
&lt;p&gt;💡 Kafka can support most integration workloads, but its architectural model introduces trade-offs that architects must understand. The most successful implementations are those that align Kafka's strengths with business requirements, rather than assuming that a streaming platform is the optimal solution for every messaging scenario.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30143/can-kafka-handle-every-integration-use-case-understanding-the-architectural-trade-offs</guid>
<pubDate>Wed, 07 Oct 2026 13:39:23 +0000</pubDate>
</item>
<item>
<title>Building Event-Driven Autonomous Agents (Zyro)</title>
<link>https://coderlegion.com/30128/building-event-driven-autonomous-agents-zyro</link>
<description>&lt;p&gt;Hi everyone at CoderLegion. The leap from simple API wrappers to actual autonomous agents in production presents massive architectural challenges—especially when dealing with long-running tasks. I wanted to share a real-world demo of how we handle this in the backend of Zyro Workspace.&lt;/p&gt;
&lt;p&gt;In this video, I put the system to the test with an asynchronous, multi-stage workflow. Here is how our architecture handles the flow under the hood:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Inversion of Control (IoC) and Zero-Retention:&lt;br&gt;
I ask the assistant to take its draft from the whiteboard, turn it into a Google Doc, and send it via email. The AI never &quot;owns&quot; my credentials or executes code directly. Our Node.js middleware intercepts the JSON payload with its intent, validates the security, and executes the action by injecting my native OAuth2 token. The security boundaries are enforced by the infrastructure, not by a fragile System Prompt.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;True Asynchrony (Event-Driven):&lt;br&gt;
The real architectural challenge comes when I ask Zyro to call the client on the phone to confirm receipt of the email and schedule a meeting. Keeping an HTTP connection open while a human answers and negotiates would destroy any server's resources. Here we apply strict asynchrony: Zyro dispatches the task to our PBX (telephone switchboard) and frees the main thread (it &quot;goes to sleep&quot;).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;State Management and Self-Adjustment -:&lt;br&gt;
When the physical phone call ends, the PBX fires an asynchronous Webhook that &quot;wakes up&quot; Zyro by injecting the transcript. As seen in the video, the client rejects the Tuesday 10:00 appointment and negotiates for Wednesday at 11:00. The agent evaluates the new state autonomously, informs me via chat, and properly registers the final event in my Google Calendar.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This entire flow is backed by our Context Footprint Management engine. To survive such a long recursive loop (text generation, API consumption, and call transcripts) without overflowing the LLM's context window, the system purges and compacts its memory milliseconds before each new inference.&lt;/p&gt;
&lt;p&gt;The technical challenge of autonomous agents isn't making them sound smart, but building the guardrails (circuit breakers) and asynchronous state management that prevent the model from collapsing.&lt;/p&gt;
&lt;p&gt;I'd love to hear your thoughts: How are you managing long-running tasks and tool-calling loops in your AI agents in production?&lt;/p&gt;
</description>
<category>Videos</category>
<guid isPermaLink="true">https://coderlegion.com/30128/building-event-driven-autonomous-agents-zyro</guid>
<pubDate>Wed, 07 Oct 2026 11:36:23 +0000</pubDate>
</item>
<item>
<title>Building Fault-Tolerant, Event-Driven Kafka Pipelines in Go: Reliable Reprocessing &amp; Dead Letter Queues</title>
<link>https://coderlegion.com/28710/building-fault-tolerant-event-driven-kafka-pipelines-reliable-reprocessing-letter-queues</link>
<description>&lt;blockquote&gt;&lt;p&gt;A practical guide to building reliable event-driven systems in Go using Apache Kafka. Learn how to implement tiered retry strategies with delayed reprocessing, route permanently failed messages to dead letter queues in Golang with Sarama.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h3&gt;
&lt;p&gt;What do you need to follow along?&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Working knowledge of Golang.&lt;/li&gt;
&lt;li&gt;Go &amp;amp; Docker installed on your PC.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;what-is-an-event-driven-architecture-&quot;&gt;What is an Event-Driven Architecture?&lt;/h3&gt;
&lt;p&gt;An Event-Driven Architecture (EDA) is a design approach where services communicate by producing and responding to events. Each service operates independently, producing or reacting to events as they happen.&lt;/p&gt;
&lt;h4 id=&quot;what-are-events-&quot;&gt;What are Events?&lt;/h4&gt;
&lt;p&gt;An event is a record of something that has happened in a system, typically representing a state change or a significant action. An event contains data (payload) describing what happened. An example of an event could be:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A user signing up for a service.&lt;/li&gt;
&lt;li&gt;A user placing an order in your system.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;components-of-an-event-driven-architecture&quot;&gt;Components of an Event-Driven Architecture&lt;/h4&gt;
&lt;p&gt;To understand how events flow through a system, we need to know three key players:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Event Producers&lt;/strong&gt;: They are the sources of events. They generate and publish events like signup events, order placed events, etc. Producers generate events and transmit them to the rest of the system. They do not know who is listening for or handling the events.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Event Brokers&lt;/strong&gt;: They sit between producers and consumers, decoupling them so neither needs a direct connection to the other. Brokers receive event messages, maintain their chronological order, make them available for consumption, and route them to the right consumers. &lt;a rel=&quot;nofollow&quot; href=&quot;https://kafka.apache.org/&quot;&gt;Apache Kafka&lt;/a&gt; is an example of an event broker, and it's the one we'll use throughout this guide.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Event Consumers&lt;/strong&gt;: They handle the processing tasks. They listen on event channels and react when an event they are subscribed to is published, then they process the event, which can include making API calls, updating a database, triggering other events, or logging information.&lt;/p&gt;
&lt;h4 id=&quot;the-complete-flow&quot;&gt;The Complete Flow&lt;/h4&gt;
&lt;p&gt;With those three pieces in place, the flow of an EDA looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When an app or service performs an action that another part of the system needs to know about, it publishes a new event to the broker.&lt;/li&gt;
&lt;li&gt;The broker receives the event, maintains the event's order relative to others in the stream, and then routes it to the appropriate consumer.&lt;/li&gt;
&lt;li&gt;An event consumer then ingests the message in real time or at a later relevant instance and processes it to trigger another action.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;apache-kafka-defined&quot;&gt;Apache Kafka, Defined&lt;/h3&gt;
&lt;p&gt;Apache Kafka is an open-source, distributed, event-streaming platform used to publish, store, process, and consume real-time data streams. It is based on a publish-subscribe messaging model and is designed to support fault-tolerant, scalable, high-throughput, and low-latency data pipelines, event-driven applications, and stream-processing systems.&lt;/p&gt;
&lt;h4 id=&quot;key-apache-kafka-components&quot;&gt;Key Apache Kafka Components&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Messages&lt;/strong&gt;: A unit of data comprised of two parts: a key and a value. The value is the actual content being sent, while the key identifies what the message relates to.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Producers&lt;/strong&gt;: Applications that publish events or messages to Kafka topics. A producer can publish to one or more topics and can optionally choose the partition that stores the data.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Topics&lt;/strong&gt;: A topic is a named channel or routing mechanism through which events are published and delivered to consumers. Applications write to and read from topics.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Partitions&lt;/strong&gt;: Subdivisions of a topic that distribute records across multiple brokers. Partitions let Kafka scale horizontally while preserving record order within each partition.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Brokers&lt;/strong&gt;: Kafka servers that store topic partitions, handle client requests, and replicate data across the cluster for fault tolerance.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Consumers&lt;/strong&gt;: Applications that subscribe to topics and read records from partitions.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Consumer Groups&lt;/strong&gt;: Allow multiple consumers to work together to process records from a topic.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Offsets&lt;/strong&gt;: Unique sequential identifiers assigned to records within a partition. Consumers use offsets to track their reading position and replay data when needed.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Together, these components allow organizations to reliably stream, process, and distribute high-volume data in real time across complex, distributed environments.&lt;/p&gt;
&lt;h3 id=&quot;setting-up-kafka-locally-with-docker&quot;&gt;Setting Up Kafka Locally with Docker&lt;/h3&gt;
&lt;p&gt;Before we write any Go code, we need a running Kafka cluster. We will use Docker Compose to spin up Kafka locally:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  broker:
    image: apache/kafka:latest
    hostname: broker
    container_name: broker
    ports:
      - 9092:9092
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_NODE_ID: 1
      KAFKA_PROCESS_ROLES: broker,controller
      CLUSTER_ID: 59Uj60pTdxFKv5bUMDYZFA
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT,CONTROLLER:PLAINTEXT
      KAFKA_LISTENERS: PLAINTEXT://broker:29092,CONTROLLER://broker:29093,PLAINTEXT_HOST://0.0.0.0:9092
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://broker:29092,PLAINTEXT_HOST://localhost:9092
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@broker:29093
      KAFKA_LOG_DIRS: /tmp/kraft-combined-logs
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since the introduction of &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.confluent.io/platform/current/kafka-metadata/kraft.html&quot;&gt;KRaft&lt;/a&gt;, Kafka no longer requires &lt;a rel=&quot;nofollow&quot; href=&quot;https://zookeeper.apache.org/&quot;&gt;Apache ZooKeeper®&lt;/a&gt; for managing cluster metadata. It uses Kafka itself instead. One advantage of the new KRaft mode is that you can have a single Kafka broker to handle both metadata and client requests in a small, local development environment. The docker-compose.yml file for this tutorial uses this approach, leading to faster startup times and simpler configuration. Note that, in a production setting, you'll have distinct Kafka brokers for handling requests and operating as a cluster controller.&lt;/p&gt;
&lt;p&gt;Start the cluster with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;defining-topics-and-consumer-groups&quot;&gt;Defining Topics and Consumer Groups&lt;/h3&gt;
&lt;p&gt;Before we build the producer and consumer, let's define the topics and consumer groups that both sides of our pipeline will share. We will keep these in a separate &lt;code&gt;pkg/topics&lt;/code&gt; package so both the producer and consumer can import them:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// pkg/topics/topics.go

package topics

type Topics string

const (
	ORDERS_CREATED Topics = &quot;orders_created&quot;
	RETRY_5M       Topics = &quot;retry_5m&quot;
	RETRY_20M      Topics = &quot;retry_20m&quot;
	RETRY_40M      Topics = &quot;retry_40m&quot;
	ORDERS_DLQ     Topics = &quot;orders_dlq&quot;
)

func (t Topics) String() string {
	return string(t)
}

type ConsumerGroup string

const (
	ORDERS_MAIN_GROUP      ConsumerGroup = &quot;orders_main_group&quot;
	ORDERS_5M_RETRY_GROUP  ConsumerGroup = &quot;orders_retry_5m_group&quot;
	ORDERS_20M_RETRY_GROUP ConsumerGroup = &quot;orders_retry_20m_group&quot;
	ORDERS_40M_RETRY_GROUP ConsumerGroup = &quot;orders_retry_40m_group&quot;
)

func (t ConsumerGroup) String() string {
	return string(t)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We have a main topic for fresh orders, three retry topics with increasing delays (5 minutes, 20 minutes, 40 minutes), and a dead letter queue (DLQ) for messages that have exhausted all retry attempts. The consumer groups map one-to-one with these topics so each tier has its own independent group.&lt;/p&gt;
&lt;h3 id=&quot;creating-the-producer&quot;&gt;Creating the Producer&lt;/h3&gt;
&lt;p&gt;Now that Kafka is running and our shared definitions are in place, let's build the producer side of our pipeline. The producer is responsible for receiving an order request via a REST API and publishing that event to our Kafka broker.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;We'll be using Sarama throughout this guide, Sarama is an MIT-licensed Go client library for Apache Kafka.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;connecting-to-kafka&quot;&gt;Connecting to Kafka&lt;/h4&gt;
&lt;p&gt;First, we need a function that establishes a connection to our Kafka broker and returns a synchronous producer:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func ConnectProducer(brokersUrl []string) (sarama.SyncProducer, error) {
	config := sarama.NewConfig()

	sarama.MaxRequestSize = 100 * 1024 * 1024
	sarama.MaxResponseSize = 100 * 1024 * 1024
	config.Producer.MaxMessageBytes = 100 * 1024 * 1024

	config.Producer.Return.Successes = true
	config.Producer.RequiredAcks = sarama.WaitForAll
	config.Producer.Idempotent = true
	config.Producer.Retry.Max = 3
	config.Net.MaxOpenRequests = 1

	conn, err := sarama.NewSyncProducer(brokersUrl, config)
	if err != nil {
		return nil, err
	}

	return conn, nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A few things worth noting here. &lt;code&gt;RequiredAcks = sarama.WaitForAll&lt;/code&gt; means the producer will wait for all in-sync replicas to acknowledge the message before considering it successfully sent. We also set &lt;code&gt;Idempotent = true&lt;/code&gt; to prevent duplicate messages in case of retries, and &lt;code&gt;MaxOpenRequests = 1&lt;/code&gt; is required for idempotent producers to guarantee ordering.&lt;/p&gt;
&lt;h4 id=&quot;publishing-messages&quot;&gt;Publishing Messages&lt;/h4&gt;
&lt;p&gt;Next, our publisher function which produces a message to our broker:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func PushOrderToQueue(producer sarama.SyncProducer, topic string, message []byte) error {

	msg := &amp;amp;sarama.ProducerMessage{
		Topic: topic,
		Value: sarama.StringEncoder(message),
	}

	partition, offset, err := producer.SendMessage(msg)
	if err != nil {
		return err
	}

	fmt.Printf(&quot;Message is stored in topic(%s)/partition(%d)/offset(%d)&quot;, topic, partition, offset)
	return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-rest-api-entry-point&quot;&gt;The REST API Entry Point&lt;/h4&gt;
&lt;p&gt;We will use a basic REST API POST request to handle incoming user requests that will then trigger our producer to publish an event to the broker:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func main() {

	brokersUrl := []string{&quot;localhost:9092&quot;}

	producer, err := ConnectProducer(brokersUrl)
	if err != nil {
		log.Fatalf(&quot;Failed to initialize Kafka producer: %v&quot;, err)
	}
	defer producer.Close()

	router := gin.Default()

	api := router.Group(&quot;/api/v1&quot;)

	api.POST(&quot;/order&quot;, createOrderHandler(producer))

	router.Run()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-order-handler&quot;&gt;The Order Handler&lt;/h4&gt;
&lt;p&gt;Now our &lt;code&gt;createOrderHandler&lt;/code&gt;, which will produce the event on success:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type Order struct {
	Text string `form:&quot;text&quot; json:&quot;text&quot;`
}

func createOrderHandler(producer sarama.SyncProducer) gin.HandlerFunc {
	return func(c *gin.Context) {
		topic := topics.ORDERS_CREATED.String()
		order := new(Order)

		if err := c.ShouldBindJSON(&amp;amp;order); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{&quot;error&quot;: err.Error()})
			return
		}

		orderInBytes, err := json.Marshal(order)
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{&quot;error&quot;: err.Error()})
			return
		}

		err = PushOrderToQueue(producer, topic, orderInBytes)
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{&quot;error&quot;: err.Error()})
			return
		}

		c.JSON(http.StatusOK, gin.H{
			&quot;success&quot;: true,
			&quot;message&quot;: &quot;Order pushed successfully&quot;,
			&quot;order&quot;:   order,
		})
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;complete-producer-code&quot;&gt;Complete Producer Code&lt;/h4&gt;
&lt;p&gt;Here is the full producer code with everything wired together:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// producer/producer.go

package main

import (
	&quot;encoding/json&quot;
	&quot;fmt&quot;
	&quot;log&quot;
	&quot;net/http&quot;

	&quot;github.com/IBM/sarama&quot;
	&quot;github.com/gin-gonic/gin&quot;
	&quot;github.com/umohsamuel/edd-kafka-golang/pkg/topics&quot;
)

type Order struct {
	Text string `form:&quot;text&quot; json:&quot;text&quot;`
}

func main() {

	brokersUrl := []string{&quot;localhost:9092&quot;}

	producer, err := ConnectProducer(brokersUrl)
	if err != nil {
		log.Fatalf(&quot;Failed to initialize Kafka producer: %v&quot;, err)
	}
	defer producer.Close()

	router := gin.Default()

	api := router.Group(&quot;/api/v1&quot;)

	api.POST(&quot;/order&quot;, createOrderHandler(producer))

	router.Run()
}

func ConnectProducer(brokersUrl []string) (sarama.SyncProducer, error) {
	config := sarama.NewConfig()

	sarama.MaxRequestSize = 100 * 1024 * 1024
	sarama.MaxResponseSize = 100 * 1024 * 1024
	config.Producer.MaxMessageBytes = 100 * 1024 * 1024

	config.Producer.Return.Successes = true
	config.Producer.RequiredAcks = sarama.WaitForAll
	config.Producer.Idempotent = true
	config.Producer.Retry.Max = 3
	config.Net.MaxOpenRequests = 1

	conn, err := sarama.NewSyncProducer(brokersUrl, config)
	if err != nil {
		return nil, err
	}

	return conn, nil

}

func PushOrderToQueue(producer sarama.SyncProducer, topic string, message []byte) error {

	msg := &amp;amp;sarama.ProducerMessage{
		Topic: topic,
		Value: sarama.StringEncoder(message),
	}

	partition, offset, err := producer.SendMessage(msg)
	if err != nil {
		return err
	}

	fmt.Printf(&quot;Message is stored in topic(%s)/partition(%d)/offset(%d)&quot;, topic, partition, offset)
	return nil
}

func createOrderHandler(producer sarama.SyncProducer) gin.HandlerFunc {
	return func(c *gin.Context) {
		topic := topics.ORDERS_CREATED.String()
		order := new(Order)

		if err := c.ShouldBindJSON(&amp;amp;order); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{&quot;error&quot;: err.Error()})
			return
		}

		orderInBytes, err := json.Marshal(order)
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{&quot;error&quot;: err.Error()})
			return
		}

		err = PushOrderToQueue(producer, topic, orderInBytes)
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{&quot;error&quot;: err.Error()})
			return
		}

		c.JSON(http.StatusOK, gin.H{
			&quot;success&quot;: true,
			&quot;message&quot;: &quot;Order pushed successfully&quot;,
			&quot;order&quot;:   order,
		})

	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;creating-the-consumer&quot;&gt;Creating the Consumer&lt;/h3&gt;
&lt;p&gt;Now for the other side of the pipeline. The consumer listens for events on topics, processes them, and handles failures by routing messages through a tiered reprocessing system.&lt;/p&gt;
&lt;h4 id=&quot;connecting-consumer-groups&quot;&gt;Connecting Consumer Groups&lt;/h4&gt;
&lt;p&gt;Next, let's create our consumer group and producer connections. The consumer side also needs a producer because when a message fails processing, it needs to publish the message to the next retry topic:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func ConnectConsumerGroup(brokersUrl []string, groupID string) (sarama.ConsumerGroup, error) {
	config := sarama.NewConfig()

	config.Consumer.Return.Errors = true
	config.Consumer.Offsets.AutoCommit.Enable = false
	config.Consumer.Offsets.Initial = sarama.OffsetOldest

	return sarama.NewConsumerGroup(brokersUrl, groupID, config)
}

func ConnectProducer(brokersUrl []string) (sarama.SyncProducer, error) {
	config := sarama.NewConfig()

	config.Producer.RequiredAcks = sarama.WaitForAll
	config.Producer.Return.Successes = true
	config.Producer.Idempotent = true
	config.Net.MaxOpenRequests = 1

	return sarama.NewSyncProducer(brokersUrl, config)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pay attention to &lt;code&gt;config.Consumer.Offsets.AutoCommit.Enable = false&lt;/code&gt;. By default, Sarama will automatically commit offsets in the background on a timer, which means it could mark a message as &quot;done&quot; before your code has actually finished processing it. If your app crashes between the auto-commit and the actual processing, that message is lost. By disabling auto-commit, we take full control of when offsets get committed. This means we have to manually call &lt;code&gt;session.MarkMessage&lt;/code&gt; and &lt;code&gt;session.Commit&lt;/code&gt; in our handler to commit an offset.&lt;/p&gt;
&lt;h4 id=&quot;the-main-consumer-group-handler&quot;&gt;The Main Consumer Group Handler&lt;/h4&gt;
&lt;p&gt;This is where the processing logic lives. When a message is consumed, we will attempt to process it. If processing fails, we route the message to the next retry tier:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type ConsumerGroupHandler struct {
	producer sarama.SyncProducer
}

func (h *ConsumerGroupHandler) Setup(sarama.ConsumerGroupSession) error {
	return nil
}

func (h *ConsumerGroupHandler) Cleanup(sarama.ConsumerGroupSession) error {
	return nil
}

func (h *ConsumerGroupHandler) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {

	for msg := range claim.Messages() {
		fmt.Printf(&quot;Received message: key=%s, value=%s, partition=%d, offset=%d\n&quot;, string(msg.Key), string(msg.Value), msg.Partition, msg.Offset)

		err := processBusinessLogic(msg.Value)

		if err != nil {

			log.Printf(&quot;Processing failed on topic %s. Routing to next stage...&quot;, msg.Topic)

			errRoute := h.routeToNextStage(msg)
			if errRoute != nil {
				log.Printf(&quot;CRITICAL: Failed to route message: %v&quot;, errRoute)
				return errRoute
			}
		}

		session.MarkMessage(msg, &quot;&quot;)
		session.Commit()
	}

	return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice how after we are done with a message, whether it was processed successfully or routed to the next retry stage, we call &lt;code&gt;session.MarkMessage(msg, &quot;&quot;)&lt;/code&gt; followed by &lt;code&gt;session.Commit()&lt;/code&gt;. This is the manual offset commit we set up earlier when we disabled auto-commit. &lt;code&gt;MarkMessage&lt;/code&gt; tells Sarama &quot;I am done with this message&quot; and &lt;code&gt;Commit&lt;/code&gt; actually persists that offset to Kafka. This way, if the consumer crashes mid-processing, Kafka knows to redeliver the message because the offset was never committed.&lt;/p&gt;
&lt;h4 id=&quot;routing-failed-messages&quot;&gt;Routing Failed Messages&lt;/h4&gt;
&lt;p&gt;The &lt;code&gt;routeToNextStage&lt;/code&gt; method is the heart of our retry strategy. It reads the &lt;code&gt;x-retry-count&lt;/code&gt; header from the message to determine how many times this message has already been retried, then routes it to the next topic accordingly. After three failed attempts, the message lands in the DLQ:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func (h *ConsumerGroupHandler) routeToNextStage(msg *sarama.ConsumerMessage) error {
	retryCount := 0
	for _, header := range msg.Headers {
		if string(header.Key) == &quot;x-retry-count&quot; {
			retryCount, _ = strconv.Atoi(string(header.Value))
		}
	}

	var nextTopic string
	switch retryCount {
	case 0:
		nextTopic = topics.RETRY_5M.String()
	case 1:
		nextTopic = topics.RETRY_20M.String()
	case 2:
		nextTopic = topics.RETRY_40M.String()
	default:
		nextTopic = topics.ORDERS_DLQ.String()
	}

	now := time.Now()
	headers := []sarama.RecordHeader{
		{Key: []byte(&quot;x-retry-count&quot;), Value: []byte(strconv.Itoa(retryCount + 1))},
		{Key: []byte(&quot;x-failed-at&quot;), Value: []byte(now.Format(time.RFC3339))},
		{Key: []byte(&quot;x-original-topic&quot;), Value: []byte(msg.Topic)},
	}

	retryMsg := &amp;amp;sarama.ProducerMessage{
		Topic:   nextTopic,
		Key:     sarama.ByteEncoder(msg.Key),
		Value:   sarama.ByteEncoder(msg.Value),
		Headers: headers,
	}

	log.Printf(&quot;Routing message to topic: %s (Attempt #%d)&quot;, nextTopic, retryCount+1)
	_, _, err := h.producer.SendMessage(retryMsg)
	return err
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We attach three headers to every retried message: &lt;code&gt;x-retry-count&lt;/code&gt; so the next handler knows which attempt this is, &lt;code&gt;x-failed-at&lt;/code&gt; so the retry handler can calculate the correct delay, and &lt;code&gt;x-original-topic&lt;/code&gt; for traceability. The switch statement maps the retry count to the appropriate next topic, escalating through 5 minute, 20 minute, and 40 minute delays before finally landing in the DLQ.&lt;/p&gt;
&lt;h4 id=&quot;the-retry-consumer-group-handler&quot;&gt;The Retry Consumer Group Handler&lt;/h4&gt;
&lt;p&gt;The retry handler works similarly to the main handler, but with one key difference: it enforces a delay before reprocessing the message. It reads the &lt;code&gt;x-failed-at&lt;/code&gt; header to determine when the message originally failed, then calculates how much time has already passed. If the message arrived before the required delay has elapsed, it sleeps for the remaining duration. If processing fails again, it escalates to the next retry tier:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type RetryConsumerGroupHandler struct {
	producer      sarama.SyncProducer
	delayDuration time.Duration
}

func (h *RetryConsumerGroupHandler) Setup(sarama.ConsumerGroupSession) error   { return nil }
func (h *RetryConsumerGroupHandler) Cleanup(sarama.ConsumerGroupSession) error { return nil }

func (h *RetryConsumerGroupHandler) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
	for msg := range claim.Messages() {

		var failedAt time.Time
		for _, header := range msg.Headers {
			if string(header.Key) == &quot;x-failed-at&quot; {
				failedAt, _ = time.Parse(time.RFC3339, string(header.Value))
			}
		}

		timeElapsed := time.Since(failedAt)
		if timeElapsed &amp;lt; h.delayDuration {
			remainingDelay := h.delayDuration - timeElapsed
			log.Printf(&quot;[%s] Message arrived early. Sleeping for %v...&quot;, msg.Topic, remainingDelay)
			time.Sleep(remainingDelay)
		}

		err := processBusinessLogic(msg.Value)
		if err != nil {
			log.Printf(&quot;[%s] Retry failed. Escalating to next tier.&quot;, msg.Topic)

			mainHandler := &amp;amp;ConsumerGroupHandler{producer: h.producer}
			errRoute := mainHandler.routeToNextStage(msg)
			if errRoute != nil {
				return errRoute
			}
		}

		session.MarkMessage(msg, &quot;&quot;)
		session.Commit()
	}
	return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;simulating-business-logic&quot;&gt;Simulating Business Logic&lt;/h4&gt;
&lt;p&gt;For testing purposes, here is a dummy function that simulates our business logic. You can toggle between returning &lt;code&gt;nil&lt;/code&gt; (success) and returning an error (failure) to test both paths:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func processBusinessLogic(data []byte) error {
	// return nil
	return fmt.Errorf(&quot;forced simulation error&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;complete-consumer-code&quot;&gt;Complete Consumer Code&lt;/h4&gt;
&lt;p&gt;Here is the full consumer code with everything connected together. We create a shared producer (for routing failed messages), spin up consumer groups for the main topic and each retry tier, and run them all concurrently in separate goroutines:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// consumer/consumer.go

package main

import (
	&quot;context&quot;
	&quot;fmt&quot;
	&quot;log&quot;
	&quot;os&quot;
	&quot;os/signal&quot;
	&quot;strconv&quot;
	&quot;syscall&quot;
	&quot;time&quot;

	&quot;github.com/IBM/sarama&quot;
	&quot;github.com/umohsamuel/edd-kafka-golang/pkg/topics&quot;
)

func main() {
	brokersUrl := []string{&quot;localhost:9092&quot;}

	sharedProducer, err := ConnectProducer(brokersUrl)
	if err != nil {
		log.Fatalf(&quot;Failed to start DLQ Producer: %v&quot;, err)
	}
	defer sharedProducer.Close()

	mainGroup, err := ConnectConsumerGroup(brokersUrl, topics.ORDERS_MAIN_GROUP.String())
	if err != nil {
		log.Fatalf(&quot;Failed to start Main Consumer Group: %v&quot;, err)
	}
	defer mainGroup.Close()

	retryGroup5m, err := ConnectConsumerGroup(brokersUrl, topics.ORDERS_5M_RETRY_GROUP.String())
	if err != nil {
		log.Fatalf(&quot;Failed to start 5m Retry Group: %v&quot;, err)
	}
	defer retryGroup5m.Close()

	retryGroup20m, err := ConnectConsumerGroup(brokersUrl, topics.ORDERS_20M_RETRY_GROUP.String())
	if err != nil {
		log.Fatalf(&quot;Failed to start 20m Retry Group: %v&quot;, err)
	}
	defer retryGroup20m.Close()

	retryGroup40m, err := ConnectConsumerGroup(brokersUrl, topics.ORDERS_40M_RETRY_GROUP.String())
	if err != nil {
		log.Fatalf(&quot;Failed to start 40m Retry Group: %v&quot;, err)
	}
	defer retryGroup40m.Close()

	ctx, cancel := context.WithCancel(context.Background())
	sigchan := make(chan os.Signal, 1)
	signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM)

	fmt.Println(&quot;Consumers have entered the fray...&quot;)

	go func() {
		handler := &amp;amp;ConsumerGroupHandler{
			producer: sharedProducer,
		}

		for {
			if err := mainGroup.Consume(ctx, []string{topics.ORDERS_CREATED.String()}, handler); err != nil {
				log.Printf(&quot;Error from main consumer group: %v&quot;, err)
			}

			if ctx.Err() != nil {
				return
			}
		}
	}()

	go func() {
		handler := &amp;amp;RetryConsumerGroupHandler{
			producer:      sharedProducer,
			delayDuration: 5 * time.Minute,
		}

		for {
			if err := retryGroup5m.Consume(ctx, []string{topics.RETRY_5M.String()}, handler); err != nil {
				log.Printf(&quot;Error from %s group: %v&quot;, topics.RETRY_5M.String(), err)
			}

			if ctx.Err() != nil {
				return
			}
		}
	}()

	go func() {
		handler := &amp;amp;RetryConsumerGroupHandler{
			producer:      sharedProducer,
			delayDuration: 20 * time.Minute,
		}

		for {
			if err := retryGroup20m.Consume(ctx, []string{topics.RETRY_20M.String()}, handler); err != nil {
				log.Printf(&quot;Error from %s group: %v&quot;, topics.RETRY_20M.String(), err)
			}

			if ctx.Err() != nil {
				return
			}
		}
	}()

	go func() {
		handler := &amp;amp;RetryConsumerGroupHandler{
			producer:      sharedProducer,
			delayDuration: 40 * time.Minute,
		}

		for {
			if err := retryGroup40m.Consume(ctx, []string{topics.RETRY_40M.String()}, handler); err != nil {
				log.Printf(&quot;Error from %s group: %v&quot;, topics.RETRY_40M.String(), err)
			}

			if ctx.Err() != nil {
				return
			}
		}
	}()

	&amp;lt;-sigchan
	fmt.Println(&quot;Interruption detected, shutting down all groups gracefully...&quot;)
	cancel()

}

func ConnectConsumerGroup(brokersUrl []string, groupID string) (sarama.ConsumerGroup, error) {
	config := sarama.NewConfig()

	config.Consumer.Return.Errors = true
	config.Consumer.Offsets.AutoCommit.Enable = false
	config.Consumer.Offsets.Initial = sarama.OffsetOldest

	return sarama.NewConsumerGroup(brokersUrl, groupID, config)
}

func ConnectProducer(brokersUrl []string) (sarama.SyncProducer, error) {
	config := sarama.NewConfig()

	config.Producer.RequiredAcks = sarama.WaitForAll
	config.Producer.Return.Successes = true
	config.Producer.Idempotent = true
	config.Net.MaxOpenRequests = 1

	return sarama.NewSyncProducer(brokersUrl, config)
}

func processBusinessLogic(data []byte) error {
	// return nil
	return fmt.Errorf(&quot;forced simulation error&quot;)
}

type ConsumerGroupHandler struct {
	producer sarama.SyncProducer
}

func (h *ConsumerGroupHandler) Setup(sarama.ConsumerGroupSession) error {
	return nil
}

func (h *ConsumerGroupHandler) Cleanup(sarama.ConsumerGroupSession) error {
	return nil
}

func (h *ConsumerGroupHandler) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {

	for msg := range claim.Messages() {
		fmt.Printf(&quot;Received message: key=%s, value=%s, partition=%d, offset=%d\n&quot;, string(msg.Key), string(msg.Value), msg.Partition, msg.Offset)

		err := processBusinessLogic(msg.Value)

		if err != nil {

			log.Printf(&quot;Processing failed on topic %s. Routing to next stage...&quot;, msg.Topic)

			errRoute := h.routeToNextStage(msg)
			if errRoute != nil {
				log.Printf(&quot;CRITICAL: Failed to route message: %v&quot;, errRoute)
				return errRoute
			}
		}

		session.MarkMessage(msg, &quot;&quot;)
		session.Commit()
	}

	return nil
}

func (h *ConsumerGroupHandler) routeToNextStage(msg *sarama.ConsumerMessage) error {
	retryCount := 0
	for _, header := range msg.Headers {
		if string(header.Key) == &quot;x-retry-count&quot; {
			retryCount, _ = strconv.Atoi(string(header.Value))
		}
	}

	var nextTopic string
	switch retryCount {
	case 0:
		nextTopic = topics.RETRY_5M.String()
	case 1:
		nextTopic = topics.RETRY_20M.String()
	case 2:
		nextTopic = topics.RETRY_40M.String()
	default:
		nextTopic = topics.ORDERS_DLQ.String()
	}

	now := time.Now()
	headers := []sarama.RecordHeader{
		{Key: []byte(&quot;x-retry-count&quot;), Value: []byte(strconv.Itoa(retryCount + 1))},
		{Key: []byte(&quot;x-failed-at&quot;), Value: []byte(now.Format(time.RFC3339))},
		{Key: []byte(&quot;x-original-topic&quot;), Value: []byte(msg.Topic)},
	}

	retryMsg := &amp;amp;sarama.ProducerMessage{
		Topic:   nextTopic,
		Key:     sarama.ByteEncoder(msg.Key),
		Value:   sarama.ByteEncoder(msg.Value),
		Headers: headers,
	}

	log.Printf(&quot;Routing message to topic: %s (Attempt #%d)&quot;, nextTopic, retryCount+1)
	_, _, err := h.producer.SendMessage(retryMsg)
	return err
}

type RetryConsumerGroupHandler struct {
	producer      sarama.SyncProducer
	delayDuration time.Duration
}

func (h *RetryConsumerGroupHandler) Setup(sarama.ConsumerGroupSession) error   { return nil }
func (h *RetryConsumerGroupHandler) Cleanup(sarama.ConsumerGroupSession) error { return nil }

func (h *RetryConsumerGroupHandler) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
	for msg := range claim.Messages() {

		var failedAt time.Time
		for _, header := range msg.Headers {
			if string(header.Key) == &quot;x-failed-at&quot; {
				failedAt, _ = time.Parse(time.RFC3339, string(header.Value))
			}
		}

		timeElapsed := time.Since(failedAt)
		if timeElapsed &amp;lt; h.delayDuration {
			remainingDelay := h.delayDuration - timeElapsed
			log.Printf(&quot;[%s] Message arrived early. Sleeping for %v...&quot;, msg.Topic, remainingDelay)
			time.Sleep(remainingDelay)
		}

		err := processBusinessLogic(msg.Value)
		if err != nil {
			log.Printf(&quot;[%s] Retry failed. Escalating to next tier.&quot;, msg.Topic)

			mainHandler := &amp;amp;ConsumerGroupHandler{producer: h.producer}
			errRoute := mainHandler.routeToNextStage(msg)
			if errRoute != nil {
				return errRoute
			}
		}

		session.MarkMessage(msg, &quot;&quot;)
		session.Commit()
	}
	return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;running-the-application&quot;&gt;Running the Application&lt;/h3&gt;
&lt;p&gt;To run the application, first start the Docker Compose cluster (if you already did this earlier, you can skip it), then run the producer and consumer in separate terminals:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd producer &amp;amp;&amp;amp; go run .
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd consumer &amp;amp;&amp;amp; go run .
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;testing-with-curl&quot;&gt;Testing with cURL&lt;/h3&gt;
&lt;p&gt;You can test the pipeline by sending a POST request to the producer's API:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -X POST http://localhost:8080/api/v1/order \
  -H &quot;Content-Type: application/json&quot; \
  -d '{&quot;text&quot;: &quot;New order from Lagos&quot;}'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To see the full retry flow in action, keep the &lt;code&gt;processBusinessLogic&lt;/code&gt; function returning the dummy error. Send a request and watch the consumer logs as the message fails, gets routed to the 5 minute retry topic, fails again, moves to the 20 minute retry, then the 40 minute retry, and finally lands in the DLQ.&lt;/p&gt;
&lt;p&gt;Once you have confirmed that flow, change &lt;code&gt;processBusinessLogic&lt;/code&gt; to return &lt;code&gt;nil&lt;/code&gt;, restart the consumer service, and send another request to confirm the success path works cleanly, with the message being marked and committed on the first pass.&lt;/p&gt;
&lt;p&gt;Lastly, while the consumer service is running, try hitting &lt;code&gt;Ctrl+C&lt;/code&gt; as well. You should see each consumer group shut down gracefully in turn, without dropping any in-flight messages.&lt;/p&gt;
&lt;h3 id=&quot;demo&quot;&gt;Demo&lt;/h3&gt;
&lt;p&gt;Here is a demo of how all these concepts come together:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://res.cloudinary.com/db6nohcui/video/upload/v1786899332/c79e8f83-1540-4b9b-8262-d7b6798bea5a_edd-golang-kafka_tnlpk0.mp4&quot;&gt;Demo Link&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;In this guide, we built a fault-tolerant event-driven pipeline using Apache Kafka and Go. The key takeaways are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Event-Driven Architecture&lt;/strong&gt; decouples your services, letting producers and consumers operate independently through a message broker.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tiered retry topics &amp;amp; reprocessing&lt;/strong&gt; with increasing delays (5m, 20m, 40m) give transient failures time to resolve before reprocessing, instead of hammering the same operation repeatedly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dead letter queues&lt;/strong&gt; catch messages that have exhausted all retry attempts, so they can be inspected and handled manually without blocking the pipeline.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consumer groups&lt;/strong&gt; let you scale consumption horizontally while Kafka handles partition assignment and offset tracking.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These patterns apply to any Go service that needs reliable, asynchronous message processing, whether you are processing orders, sending notifications, or syncing data between services.&lt;/p&gt;
&lt;p&gt;(If you want to see the full codebase, checkout the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/umohsamuel/edd-kafka-golang&quot;&gt;repository&lt;/a&gt;. Play around with it and lmk what you think)&lt;/p&gt;
&lt;h3 id=&quot;references&quot;&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.uber.com/us/en/blog/reliable-reprocessing/&quot;&gt;Uber Engineering: Reliable Reprocessing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.confluent.io/confluent-tutorials/kafka-on-docker/&quot;&gt;Confluent: Kafka on Docker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.tencentcloud.com/document/product/597/60360&quot;&gt;Tencent Cloud: Kafka Dead Letter Queue&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.confluent.io/learn/kafka-dead-letter-queue/#overview&quot;&gt;Confluent: Kafka Dead Letter Queue Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.ibm.com/think/topics/apache-kafka&quot;&gt;IBM: Apache Kafka&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.ibm.com/think/topics/event-driven-architecture&quot;&gt;IBM: Event-Driven Architecture&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://kafka.apache.org/43/getting-started/introduction/&quot;&gt;Apache Kafka: Getting Started&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28710/building-fault-tolerant-event-driven-kafka-pipelines-reliable-reprocessing-letter-queues</guid>
<pubDate>Sat, 26 Sep 2026 10:35:10 +0000</pubDate>
</item>
<item>
<title>Kafka in hospitals: real-world event architecture for HIS, LIS and billing in a Venezuelan mid-size</title>
<link>https://coderlegion.com/23724/kafka-hospitals-real-world-event-architecture-for-his-lis-and-billing-venezuelan-mid-size</link>
<description>&lt;p&gt;Most Kafka “healthcare interoperability” examples are theoretical or focused on US/EU EHRs. I wanted to share a concrete case from a mid‑size hospital in Venezuela where we used Kafka as the event backbone between HIS, LIS and billing systems.&lt;/p&gt;
&lt;p&gt;Context:&lt;/p&gt;
&lt;p&gt;4–8 systems needed to know the same things in near real time: admissions, lab results, charges.&lt;/p&gt;
&lt;p&gt;Point‑to‑point integrations had become a mess: custom scripts, duplicate records, fragile error handling.&lt;/p&gt;
&lt;p&gt;The hospital knew it would keep adding systems (BI, SAP, new clinical modules) over the next few years.&lt;/p&gt;
&lt;p&gt;What we ended up doing:&lt;/p&gt;
&lt;p&gt;Defined topics by clinical domain (admissions, lab-results, orders, billing-events) instead of “one topic for everything”.&lt;/p&gt;
&lt;p&gt;Partitioned by patient or encounter ID to preserve clinical event order per case.&lt;/p&gt;
&lt;p&gt;Used replication factor 3 and min.insync.replicas=2 because losing a lab result is not just a business data issue.&lt;/p&gt;
&lt;p&gt;Enabled idempotent + transactional producers (exactly-once semantics) for lab results and financial events, where duplicates are harmful.&lt;/p&gt;
&lt;p&gt;The article goes into more detail on:&lt;/p&gt;
&lt;p&gt;How we mapped HL7/FHIR messages to Kafka topics.&lt;/p&gt;
&lt;p&gt;Trade‑offs in infra cost vs. resilience for a hospital context.&lt;/p&gt;
&lt;p&gt;When Kafka was clearly the wrong tool (small clinics with only 2–3 systems and low event volume).&lt;/p&gt;
&lt;p&gt;Link (in Spanish, but I’m happy to summarize or answer questions in English here):&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://codebymelendez.com/insights/apache-kafka-sistemas-hospitalarios&quot;&gt;Kafka in hospitals&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23724/kafka-hospitals-real-world-event-architecture-for-his-lis-and-billing-venezuelan-mid-size</guid>
<pubDate>Wed, 29 Jul 2026 06:37:11 +0000</pubDate>
</item>
<item>
<title>Message Queue Example using RabbitMQ in Go</title>
<link>https://coderlegion.com/16472/message-queue-example-using-rabbitmq-in-go</link>
<description>&lt;h3 id=&quot;project-overview&quot;&gt;Project Overview&lt;/h3&gt;
&lt;p&gt;In this example, we will build a simple advertisement platform. This platform will send a notification if the advertisement is created. The message queue is utilized as a buffer between the advertisement service and the notification service.&lt;/p&gt;
&lt;p&gt;This is the architecture diagram of this project.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gb87t55l8e85sidt0hu5.png&quot; alt=&quot;Advertisement Platform Diagram for Message Queue example with RabbitMQ and Go&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;develop-the-project&quot;&gt;Develop the Project&lt;/h3&gt;
&lt;h4 id=&quot;project-setup&quot;&gt;Project Setup&lt;/h4&gt;
&lt;p&gt;The project is implemented using microservices architecture. These are the key components of the project:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Advertisement service: the service to handle advertisement feature including create a new advertisement.&lt;/li&gt;
&lt;li&gt;Notification service: the service for handling the advertisement notification.&lt;/li&gt;
&lt;li&gt;API Gateway: the main entrypoint for client to accessing the available services.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The project is implemented using microservices architecture. These are the key components of the project:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Advertisement service: the service to handle advertisement feature including create a new advertisement.&lt;/li&gt;
&lt;li&gt;Notification service: the service for handling the advertisement notification.&lt;/li&gt;
&lt;li&gt;API Gateway: the main entrypoint for client to accessing the available services.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The project structure is implemented using Go workspace. This is the overview of the project structure.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go-mini-ads
 ┣ ads
 ┣ common
 ┃ ┣ models
 ┃ ┣ rabbitmq
 ┃ ┣ utils
 ┣ gateway
 ┣ notification
 ┣ .env
 ┣ .gitignore
 ┣ docker-compose.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The very first step is to create a Go workspace. Initialize the Go Workspace using this command.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;go work init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After creating the workspace, notice that the &lt;code&gt;go.work&lt;/code&gt; file is created. Adjust the &lt;code&gt;go.work&lt;/code&gt; file to include the required packages.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;go 1.25.5

use (
./ads
./gateway
./notification
./common
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create the &lt;code&gt;.env&lt;/code&gt; file to store the required configurations.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;APP_MODE=&quot;production&quot;

ADS_SERVICE_BASE_URL=&quot;http://ads-service:1323&quot;

RABBITMQ_USER=&quot;admin&quot;
RABBITMQ_PASS=&quot;secretpassword123&quot;
RABBITMQ_HOST=&quot;rabbitmq-service&quot;
RABBITMQ_PORT=&quot;5672&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;If not using Docker compose, ensure to change the app mode to &lt;code&gt;dev&lt;/code&gt; and change &lt;code&gt;ads-service&lt;/code&gt; &amp;amp; &lt;code&gt;RABBITMQ_HOST&lt;/code&gt; to &lt;code&gt;localhost&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;create-common-utilities&quot;&gt;Create Common Utilities&lt;/h4&gt;
&lt;p&gt;Create a new directory called &lt;code&gt;common&lt;/code&gt;. After that, initialize a go module for this package.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;go mod init common
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Install the required packages.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;go get github.com/spf13/viper
go get github.com/rabbitmq/amqp091-go
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside the &lt;code&gt;common&lt;/code&gt; directory, create a new directory called &lt;code&gt;models&lt;/code&gt; to store required entities for the services.&lt;/p&gt;
&lt;p&gt;Inside the &lt;code&gt;models&lt;/code&gt; directory, create a new file called &lt;code&gt;ad.go&lt;/code&gt; to store the data model of the advertisement.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package models

type Ad struct {
ID       string `json:&quot;id&quot;`
Title    string `json:&quot;title&quot;`
Content  string `json:&quot;content&quot;`
Category string `json:&quot;category&quot;`
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside the &lt;code&gt;models&lt;/code&gt; directory, create a new file called &lt;code&gt;request.go&lt;/code&gt; to store the request entity for creating a new advertisement.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package models

type AdRequest struct {
Title    string `json:&quot;title&quot;`
Content  string `json:&quot;content&quot;`
Category string `json:&quot;category&quot;`
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside the &lt;code&gt;models&lt;/code&gt; directory, create a new file called &lt;code&gt;response.go&lt;/code&gt; to store the response entity. This entity is using generic which means the type of the data can be adjusted dynamically.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package models

type Response[T any] struct {
Success bool   `json:&quot;success&quot;`
Message string `json:&quot;message&quot;`
Data    T      `json:&quot;data&quot;`
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After the entities are created, create a new directory called &lt;code&gt;utils&lt;/code&gt; to store the utility functions and constants. The &lt;code&gt;utils&lt;/code&gt; package will be used across the services.&lt;/p&gt;
&lt;p&gt;Create a file called &lt;code&gt;constants.go&lt;/code&gt; to store the constant values. The constant values are used for ensuring the consistency of the value.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package utils

const ADS_QUEUE = &quot;ads&quot;

// env value keys
const (
APP_MODE             = &quot;APP_MODE&quot;
ADS_SERVICE_BASE_URL = &quot;ADS_SERVICE_BASE_URL&quot;
RABBITMQ_USER        = &quot;RABBITMQ_USER&quot;
RABBITMQ_PASS        = &quot;RABBITMQ_PASS&quot;
RABBITMQ_HOST        = &quot;RABBITMQ_HOST&quot;
RABBITMQ_PORT        = &quot;RABBITMQ_PORT&quot;
)

const (
APP_MODE_PRODUCTION = &quot;production&quot;
)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This application uses &lt;code&gt;.env&lt;/code&gt; file for storing and fetching the configurations. To fetch the values from the &lt;code&gt;.env&lt;/code&gt; file, create a new utility function called &lt;code&gt;GetConfig&lt;/code&gt; inside the &lt;code&gt;config.go&lt;/code&gt; file.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package utils

import (
&quot;log&quot;
&quot;os&quot;

&quot;github.com/spf13/viper&quot;
)

func GetConfig(key string) string {
isDevelop := os.Getenv(APP_MODE) != APP_MODE_PRODUCTION

if isDevelop {
viper.AddConfigPath(&quot;.&quot;)
viper.SetConfigFile(&quot;.env&quot;)

if err := viper.ReadInConfig(); err != nil {
log.Fatalf(&quot;error when reading configuration file: %s\n&quot;, err)
}

return viper.GetString(key)
}

return os.Getenv(key)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To interact with RabbitMQ service conveniently, create a dedicated directory called &lt;code&gt;rabbitmq&lt;/code&gt;. Inside this directory, create a new file called &lt;code&gt;rabbitmq.go&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Inside the &lt;code&gt;rabbitmq.go&lt;/code&gt;, the RabbitMQ instance will be created, this instance can initialize required setups (like: connection, queue and channel) and provides basic functionalities like publish and consume messages.&lt;/p&gt;
&lt;p&gt;First, import the required package and create a struct that act as a RabbitMQ instance.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package rabbitmq

import (
&quot;common/utils&quot;
&quot;context&quot;
&quot;errors&quot;
&quot;fmt&quot;

&quot;github.com/rabbitmq/amqp091-go&quot;
amqp &quot;github.com/rabbitmq/amqp091-go&quot;
)

type RabbitMQ struct {
Channel *amqp.Channel
Queue   amqp.Queue
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a function to retrieve the RabbitMQ address.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func GetRabbitMQAddress() string {
return fmt.Sprintf(&quot;amqp://%s:%s@%s:%s/&quot;,
utils.GetConfig(utils.RABBITMQ_USER),
utils.GetConfig(utils.RABBITMQ_PASS),
utils.GetConfig(utils.RABBITMQ_HOST),
utils.GetConfig(utils.RABBITMQ_PORT),
)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a function to initialize the RabbitMQ connection. This function returns a connection object that can be used for interacting with the RabbitMQ service.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func InitConnection(address string) (*amqp.Connection, error) {
conn, err := amqp.Dial(address)
if err != nil {
return nil, fmt.Errorf(&quot;failed to connect: %s\n&quot;, err)
}

return conn, nil
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a function to initialize the connection channel. This function returns a connection channel that can be used for using RabbitMQ features like send and consume messages.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func InitChannel(conn *amqp.Connection) (*amqp.Channel, error) {
ch, err := conn.Channel()
if err != nil {
return nil, fmt.Errorf(&quot;failed to open channel: %s\n&quot;, err)
}

return ch, nil
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a function to initialize the queue. The queue uses FIFO principle which means the first message comes in, that message will be retrieved first.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func DeclareQueue(ch *amqp.Channel) (amqp.Queue, error) {
q, err := ch.QueueDeclare(
utils.ADS_QUEUE, // name
true,            // durability
false,           // delete when unused
false,           // exclusive
false,           // no-wait
amqp.Table{
amqp.QueueTypeArg: amqp.QueueTypeQuorum,
},
)
if err != nil {
return amqp.Queue{}, fmt.Errorf(&quot;failed to create queue: %s\n&quot;, err)
}

return q, nil
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a function to initialize the RabbitMQ instance. When initializing this instance, the previous created functions are called and assembled together.&lt;/p&gt;
&lt;p&gt;Using this approach makes the code more clean and easy to refactor when there is an additional changes when initializing the instance.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func InitRabbitMQ() (*RabbitMQ, error) {
address := GetRabbitMQAddress()
conn, connErr := InitConnection(address)
ch, chErr := InitChannel(conn)
q, qErr := DeclareQueue(ch)

err := errors.Join(connErr, chErr, qErr)

if err != nil {
return nil, fmt.Errorf(&quot;failed to create RabbitMQ instance: %s\n&quot;, err)
}

return &amp;amp;RabbitMQ{
Channel: ch,
Queue:   q,
}, nil
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a method for RabbitMQ instance to publish a message to the queue. This method requires context object, queue name and the message content.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func (rq *RabbitMQ) PublishWithCtx(ctx context.Context, queueName string, body []byte) error {
return rq.Channel.PublishWithContext(ctx,
&quot;&quot;,        // exchange
queueName, // routing key
false,     // mandatory
false,     // immediate
amqp.Publishing{
ContentType: &quot;application/json&quot;,
Body:        body,
})
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a method for RabbitMQ instance to consume the messages from the queue. This method requires queue name.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func (rq *RabbitMQ) Consume(queueName string) (&amp;lt;-chan amqp091.Delivery, error) {
return rq.Channel.Consume(
queueName, // queue
&quot;&quot;,        // consumer
true,      // auto-ack
false,     // exclusive
false,     // no-local
false,     // no-wait
nil,       // args
)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;create-ads-service&quot;&gt;Create Ads Service&lt;/h4&gt;
&lt;p&gt;Create a new directory called &lt;code&gt;ads&lt;/code&gt;. After that, initialize a go module for this package.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;go mod init ads-service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Install the required packages.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;go get github.com/google/uuid
go get github.com/labstack/echo/v5
go get github.com/labstack/echo/v5/middleware
go get github.com/rabbitmq/amqp091-go
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a new file called &lt;code&gt;main.go&lt;/code&gt; to store the main logic inside this service.&lt;/p&gt;
&lt;p&gt;Import the required packages.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
&quot;common/models&quot;
&quot;common/rabbitmq&quot;
&quot;context&quot;
&quot;encoding/json&quot;
&quot;fmt&quot;
&quot;log&quot;
&quot;net/http&quot;
&quot;time&quot;

&quot;github.com/google/uuid&quot;
&quot;github.com/labstack/echo/v5&quot;
&quot;github.com/labstack/echo/v5/middleware&quot;
)

var RMQ *rabbitmq.RabbitMQ
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside the &lt;code&gt;main&lt;/code&gt; function, initialize the Echo application and RabbitMQ instance.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func main() {
var err error

e := echo.New()
e.Use(middleware.RequestLogger())

RMQ, err = rabbitmq.InitRabbitMQ()

if err != nil {
log.Fatalf(&quot;failed to init RabbitMQ: %s\n&quot;, err)
}

// create controller here

// start the server
if err := e.Start(&quot;:1323&quot;); err != nil {
e.Logger.Error(&quot;failed to start server&quot;, &quot;error&quot;, err)
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After the Echo and RabbitMQ instance are initialized, Create a controller for creating the advertisement data.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func main() {
// other codes...
e.POST(&quot;/ads/create&quot;, func(c *echo.Context) error {
req := new(models.AdRequest)

// bind request body
if err := c.Bind(req); err != nil {
return c.JSON(http.StatusBadRequest, models.Response[any]{
Success: false,
Message: &quot;request invalid&quot;,
})
}

// create advertisement record
record := models.Ad{
ID:       uuid.NewString(),
Title:    req.Title,
Content:  req.Content,
Category: req.Category,
}

// send record to the message queue
if err := sendToQueue(record); err != nil {
return c.JSON(http.StatusInternalServerError, models.Response[any]{
Success: false,
Message: err.Error(),
})
}

// return created advertisement record
return c.JSON(http.StatusCreated, models.Response[models.Ad]{
Success: true,
Message: &quot;ad created&quot;,
Data:    record,
})
})
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice that in the create advertisement controller, the &lt;code&gt;sendToQueue&lt;/code&gt; is called for sending the advertisement record to the message queue.&lt;/p&gt;
&lt;p&gt;Create a function to store the advertisement record to the message queue called &lt;code&gt;sendToQueue&lt;/code&gt;. Create this function outside the &lt;code&gt;main&lt;/code&gt; function.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func sendToQueue(data models.Ad) error {
// create context object
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// parse advertisement data
body, err := json.Marshal(data)
if err != nil {
return fmt.Errorf(&quot;failed to parse data: %s\n&quot;, err)
}

// send advertisement data to the message queue
err = RMQ.PublishWithCtx(ctx, RMQ.Queue.Name, body)
if err != nil {
return fmt.Errorf(&quot;failed to publish message: %s\n&quot;, err)
}

return nil
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;sendToQueue&lt;/code&gt; function sends the advertisement record to the message queue by calling the &lt;code&gt;PublishWithCtx&lt;/code&gt; method from the RabbitMQ instance.&lt;/p&gt;
&lt;p&gt;Create a Dockerfile for running this service as a Docker container.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM golang:1.25 AS build-stage

WORKDIR /app

COPY . .

WORKDIR /app/ads

RUN go mod download
RUN CGO_ENABLED=0 GOOS=linux go build -o /adsapp

FROM gcr.io/distroless/base-debian11 AS build-release-stage

WORKDIR /

COPY --from=build-stage /adsapp /adsapp

EXPOSE 1323

USER nonroot:nonroot

ENTRYPOINT [ &quot;./adsapp&quot; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;create-api-gateway-service&quot;&gt;Create API Gateway Service&lt;/h4&gt;
&lt;p&gt;Create a new directory called &lt;code&gt;gateway&lt;/code&gt;. After that, initialize a go module for this package.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;go mod init gateway-service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Install the required packages.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;go get github.com/labstack/echo/v5
go get github.com/labstack/echo/v5/middleware
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a &lt;code&gt;main&lt;/code&gt; function inside the &lt;code&gt;main.go&lt;/code&gt; file. Then, import the required packages.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
&quot;bytes&quot;
&quot;common/models&quot;
&quot;common/utils&quot;
&quot;encoding/json&quot;
&quot;fmt&quot;
&quot;net/http&quot;
&quot;time&quot;

&quot;github.com/labstack/echo/v5&quot;
&quot;github.com/labstack/echo/v5/middleware&quot;
)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside the &lt;code&gt;main&lt;/code&gt; function, initialize the Echo application and start the server as well.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func main() {
e := echo.New()
e.Use(middleware.RequestLogger())

e.POST(&quot;/api/ads&quot;, createAd)

if err := e.Start(&quot;:8080&quot;); err != nil {
e.Logger.Error(&quot;failed to start server&quot;, &quot;error&quot;, err)
}
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a controller called &lt;code&gt;createAd&lt;/code&gt; to create a new advertisement record. This controller sends HTTP request to the advertisement service to create a new advertisement record.&lt;/p&gt;
&lt;p&gt;Before sending the HTTP request, parse the request body and initialize the HTTP client.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func createAd(c *echo.Context) error {
adReq := new(models.AdRequest)

if err := c.Bind(adReq); err != nil {
return c.JSON(http.StatusBadRequest, models.Response[any]{
Success: false,
Message: &quot;request invalid&quot;,
})
}

// create HTTP client with 10-second request timeout
client := &amp;amp;http.Client{
Timeout: 10 * time.Second,
}

// parse request body as a JSON
body, err := json.Marshal(adReq)
if err != nil {
return c.JSON(http.StatusBadRequest, models.Response[any]{
Success: false,
Message: &quot;request invalid&quot;,
})
}

adsServiceUrl := utils.GetConfig(utils.ADS_SERVICE_BASE_URL)
adsEndpoint := fmt.Sprintf(&quot;%s/ads/create&quot;, adsServiceUrl)

// create HTTP POST request to the advertisement service
request, err := http.NewRequest(http.MethodPost, adsEndpoint, bytes.NewBuffer(body))

if err != nil {
return c.JSON(http.StatusInternalServerError, models.Response[any]{
Success: false,
Message: &quot;failed to create a request&quot;,
})
}

request.Header.Set(&quot;Content-Type&quot;, &quot;application/json&quot;)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After the request body is parsed and HTTP client is initialized, send the HTTP request and store the response body in a JSON format.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func createAd(c *echo.Context) error {
// other codes...
// send HTTP request
response, err := client.Do(request)
if err != nil || response.StatusCode != http.StatusCreated {
return c.JSON(http.StatusInternalServerError, models.Response[any]{
Success: false,
Message: &quot;failed to create ad&quot;,
})
}

// close the response body after parse process is completed
defer response.Body.Close()

// variable to store the response body
var data models.Response[models.Ad]

// parse response body and store in the &quot;data&quot; variable
err = json.NewDecoder(response.Body).Decode(&amp;amp;data)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Response[any]{
Success: false,
Message: &quot;failed to parse ad data&quot;,
})
}

return c.JSON(http.StatusCreated, data)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a Dockerfile for running this service as a Docker container.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM golang:1.25 AS build-stage

WORKDIR /app

COPY . .

WORKDIR /app/gateway

RUN go mod download
RUN CGO_ENABLED=0 GOOS=linux go build -o /gatewayapp

FROM gcr.io/distroless/base-debian11 AS build-release-stage

WORKDIR /

COPY --from=build-stage /gatewayapp /gatewayapp

EXPOSE 1323

USER nonroot:nonroot

ENTRYPOINT [ &quot;./gatewayapp&quot; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;create-notification-service&quot;&gt;Create Notification Service&lt;/h4&gt;
&lt;p&gt;Create a new directory called &lt;code&gt;notification&lt;/code&gt;. After that, initialize a go module for this package.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;go mod init notification-service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Install the required package.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;go get github.com/rabbitmq/amqp091-go
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a &lt;code&gt;main&lt;/code&gt; function inside the &lt;code&gt;main.go&lt;/code&gt; file. Then, import the required packages.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
&quot;common/models&quot;
&quot;common/rabbitmq&quot;
&quot;encoding/json&quot;
&quot;fmt&quot;
&quot;log&quot;
&quot;strings&quot;
)

var RMQ *rabbitmq.RabbitMQ
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the &lt;code&gt;main&lt;/code&gt; function, initialize the RabbitMQ instance and consume the incoming messages from the queue.&lt;/p&gt;
&lt;p&gt;The incoming messages are retrieved from the message queue via delivery channel. Consume the messages inside the goroutine.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func main() {
var err error

// initialize RabbitMQ instance
RMQ, err = rabbitmq.InitRabbitMQ()

if err != nil {
log.Fatalf(&quot;failed to init RabbitMQ: %s\n&quot;, err)
}

// consume messages
msgs, err := RMQ.Consume(RMQ.Queue.Name)

if err != nil {
log.Fatalf(&quot;failed to consume queue: %s\n&quot;, err)
}

var forever chan struct{}

// parse the messages
go func() {
for d := range msgs {
parseMessage(d.Body)
}
}()

log.Println(&quot;[*] Waiting for messages. To exit press CTRL+C&quot;)
&amp;lt;-forever
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;parseMessage&lt;/code&gt; function parses the incoming messages from the queue. This is the complete implementation of the function.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func parseMessage(body []byte) {
var record models.Ad

if err := json.Unmarshal(body, &amp;amp;record); err != nil {
log.Fatalf(&quot;failed to parse message: %s\n&quot;, err)
}

fmt.Println(&quot;send notification...&quot;)
fmt.Println(strings.Repeat(&quot;=&quot;, 5))
fmt.Println(&quot;title: &quot;, record.Title)
fmt.Println(&quot;content: &quot;, record.Content)
fmt.Println(&quot;category: &quot;, record.Category)
fmt.Println(strings.Repeat(&quot;=&quot;, 5))
fmt.Println(&quot;notification sent&quot;)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a Dockerfile for running this service as a Docker container.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM golang:1.25 AS build-stage

WORKDIR /app

COPY . .

WORKDIR /app/notification

RUN go mod download
RUN CGO_ENABLED=0 GOOS=linux go build -o /notifapp

FROM gcr.io/distroless/base-debian11 AS build-release-stage

WORKDIR /

COPY --from=build-stage /notifapp /notifapp

EXPOSE 1323

USER nonroot:nonroot

ENTRYPOINT [ &quot;./notifapp&quot; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;configure-the-services&quot;&gt;Configure the Services&lt;/h4&gt;
&lt;p&gt;The services can be run and managed easily using Docker compose. Check out the complete file &lt;a rel=&quot;nofollow&quot; href=&quot;https://gist.github.com/nadirbasalamah/3f383f3e2754cad73b7240d21aed199b&quot;&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;run-the-project&quot;&gt;Run the Project&lt;/h3&gt;
&lt;p&gt;The services can be run using this command. This command run all the specified services in the Docker compose file.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Try to send the HTTP request. This request is sent to the gateway service.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;curl -X POST &quot;http://localhost:8080/api/ads&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d '{&quot;title&quot;:&quot;new ad&quot;,&quot;content&quot;:&quot;ad content&quot;,&quot;category&quot;:&quot;anything but new&quot;}'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If successful, the notification service should be able to parse the message from the queue. Check it using &lt;code&gt;docker logs notification-service&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;send notification...
=====
title:  new ad
content:  ad content
category:  anything but new
=====
notification sent
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;sources&quot;&gt;Sources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rabbitmq.com/tutorials/tutorial-one-go&quot;&gt;Getting Started RabbitMQ with Go&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/nadirbasalamah/go-mini-ads&quot;&gt;Github Repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;⭐️ If you found this post is helpful, don't forget to like and save it. If you have any feedbacks, please let me know in the comment section down below.&lt;/p&gt;
&lt;p&gt;Thank you&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/16472/message-queue-example-using-rabbitmq-in-go</guid>
<pubDate>Sun, 03 May 2026 05:44:27 +0000</pubDate>
</item>
<item>
<title>The Evolution of a SaaS Architecture</title>
<link>https://coderlegion.com/16467/the-evolution-of-a-saas-architecture</link>
<description>&lt;p&gt;&lt;img src=&quot;https://cdn.jaiswalankit.com.np/The%20Evoluation%20of%20a%20SaaS/The%20Evolution%20of%20a%20SaaS%20-%201.jpg&quot; alt=&quot;The Evolution of a SaaS Architecture&quot;&gt;&lt;/p&gt;
&lt;p&gt;Building a scalable SaaS platform is a journey of continuous learning and strategic evolution. In modern software engineering, the most successful platforms are those built with an &quot;Evolutionary Architecture&quot; mindset. This means designing your systems to embrace change, allowing your technical capabilities to grow in perfect harmony with your user base and business milestones.&lt;/p&gt;
&lt;p&gt;When launching a new platform, the primary objective is learning about the market, discovering user needs, and establishing product-market fit. A successful architecture supports this discovery phase by prioritizing developer velocity and adaptability. By aligning your infrastructure with your current traffic levels, you ensure that engineering resources are focused entirely on delivering value to the user.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jaiswalankit.com.np/The%20Evoluation%20of%20a%20SaaS/The%20Evolution%20of%20a%20SaaS%20-%202.png&quot; alt=&quot;Match Your Tech to Your Traffic&quot;&gt;&lt;/p&gt;
&lt;p&gt;The strategy for building a highly resilient, long-lasting platform relies on a phased, educational approach. You map your infrastructure investments directly to the milestones you achieve in user growth. We can organize this philosophy into three essential pillars of technical growth:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Build for validation:&lt;/strong&gt; Design systems that allow for rapid prototyping, incredibly fast deployment cycles, and immediate user feedback.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Upgrade for stability:&lt;/strong&gt; As user acquisition accelerates, introduce foundational scaling techniques to ensure high availability and consistent response times.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Architect for scale:&lt;/strong&gt; When your platform reaches enterprise levels of engagement, systematically decouple components to unlock distributed, specialized computing power.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By adopting this progressive framework, you maintain a highly efficient engineering budget while gracefully supporting increased demand. Here is a comprehensive, deeply technical roadmap for guiding your application's journey from a unified monolith to an intelligent, event-driven microservices ecosystem.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;phase-1-the-monolithic-approach&quot;&gt;Phase 1: The Monolithic Approach&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jaiswalankit.com.np/The%20Evoluation%20of%20a%20SaaS/The%20Evolution%20of%20a%20SaaS%20-%203.png&quot; alt=&quot;Getting to know the market&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Traffic Milestone: 0 to 1,000 users.&lt;/strong&gt; &lt;strong&gt;The Tech: A Modular Monolith on a dedicated virtual environment.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;At the inception of a project, engineering speed is your greatest asset. The goal is to translate ideas into functional code as efficiently as possible to gather real-world data. A monolithic architecture is uniquely equipped to provide this rapid iteration cycle, allowing you to ship new features daily without the overhead of managing distributed systems.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jaiswalankit.com.np/The%20Evoluation%20of%20a%20SaaS/The%20Evolution%20of%20a%20SaaS%20-%204.png&quot; alt=&quot;Phase 1: The Visual&quot;&gt;&lt;/p&gt;
&lt;p&gt;In a well-designed monolithic architecture, all core functionalities your user interface serving, business logic, background jobs, and data access layers reside within a single, unified codebase and operate within the same memory space. &lt;/p&gt;
&lt;p&gt;This unified approach offers tremendous advantages for an evolving product. Deployments are straightforward, often requiring just a single CI/CD pipeline step. Because all modules share the same memory, internal function calls are lightning-fast, entirely avoiding the latency and complexity of network-based API communication. Furthermore, observing the system is beautifully simple; developers can trace a user's entire journey through the application by examining a single, centralized stream of logs. &lt;/p&gt;
&lt;p&gt;To make the most of this phase, engineering teams should focus on building a &lt;strong&gt;Modular Monolith&lt;/strong&gt;. By strictly enforcing logical boundaries within the code such as separating the user management module from the billing module you create a clean, well-organized system. This discipline not only makes the codebase easier to understand and test but also perfectly positions the application for future architectural evolution. &lt;/p&gt;
&lt;h3 id=&quot;phase-2-horizontal-scaling-and-the-12-factor-app&quot;&gt;Phase 2: Horizontal Scaling and the 12-Factor App&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jaiswalankit.com.np/The%20Evoluation%20of%20a%20SaaS/The%20Evolution%20of%20a%20SaaS%20-%205.png&quot; alt=&quot;Handling the Load&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Traffic Milestone: 1,000 to 10,000 users.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;As your platform's value resonates with the market, user engagement naturally increases. You will begin to observe higher CPU utilization and increased memory consumption as your server actively processes concurrent user requests. This is a positive milestone it signals that your platform is thriving and it is time to expand your compute capacity.&lt;/p&gt;
&lt;p&gt;To handle increased throughput gracefully, the architecture shifts from vertical scaling (adding a larger server) to horizontal scaling (adding multiple identical servers). By containerizing your application packaging the code, runtime, and dependencies into standardized units using Docker you can launch multiple instances of your application simultaneously. &lt;/p&gt;
&lt;p&gt;These instances sit behind an &lt;strong&gt;Application Load Balancer&lt;/strong&gt;. The load balancer acts as an intelligent traffic director, continuously monitoring the health of your application instances and distributing incoming HTTP requests evenly across the fleet. If traffic surges, you can simply instruct your cloud provider to spin up additional containers to share the workload.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Architectural Shift:&lt;/strong&gt; Transitioning to a horizontally scaled environment introduces a brilliant engineering concept: &lt;strong&gt;Statelessness&lt;/strong&gt;, a core principle of the 12-Factor App methodology. Because a user's requests might be routed to Server A on their first click and Server B on their second, your application servers can no longer rely on storing local data. &lt;/p&gt;
&lt;p&gt;To resolve this, engineers externalize the state. Session data, temporary cache, and user tokens are migrated to a centralized, high-speed, in-memory datastore like &lt;strong&gt;Redis&lt;/strong&gt;. By decoupling the state from the application servers, any server in your fleet can seamlessly pick up a request and process it with full context, creating a deeply resilient, highly available application tier.&lt;/p&gt;
&lt;h3 id=&quot;phase-3-optimizing-the-data-layer&quot;&gt;Phase 3: Optimizing the Data Layer&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jaiswalankit.com.np/The%20Evoluation%20of%20a%20SaaS/The%20Evolution%20of%20a%20SaaS%20-%206.png&quot; alt=&quot;Protecting the Database&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Traffic Milestone: 10,000 to 100,000 users.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Expanding your API server fleet handles computational load beautifully, but data-intensive applications eventually shift the demand to the persistent storage layer. In a thriving SaaS environment, ensuring the relational database remains responsive is paramount for a seamless user experience. &lt;/p&gt;
&lt;p&gt;Before structurally changing the database, modern engineering teams focus on &lt;strong&gt;Intelligent Caching&lt;/strong&gt;. By leveraging the Redis infrastructure introduced in Phase 2, developers can intercept frequent, heavy database queries. If thousands of users are loading the same global analytics dashboard, the database performs the complex calculation once, stores the result in Redis, and serves the subsequent thousands of requests directly from memory in a fraction of a millisecond. Mastering cache invalidation strategies ensuring users always see fresh data becomes a rewarding engineering focus at this stage.&lt;/p&gt;
&lt;p&gt;As engagement deepens, you can further enhance data availability by implementing a &lt;strong&gt;Read/Write Split&lt;/strong&gt; at the database level. &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The Master Node:&lt;/strong&gt; The primary database instance becomes highly specialized, dedicated entirely to processing data modifications handling the &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, and &lt;code&gt;DELETE&lt;/code&gt; transactions ensuring absolute data integrity.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Read Replicas:&lt;/strong&gt; You introduce synchronized copies of your database. The application intelligently routes all complex, read-only queries (&lt;code&gt;SELECT&lt;/code&gt;) to these replicas. &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This architecture prevents heavy reporting queries from consuming the resources needed for transactional operations. Your application learns to handle &quot;eventual consistency,&quot; understanding that there might be a few milliseconds of replication delay between writing to the master and reading from the replica. This elegant separation of concerns ensures that the data layer remains incredibly robust, even during your highest traffic events.&lt;/p&gt;
&lt;h3 id=&quot;phase-4-unlocking-the-event-driven-ecosystem&quot;&gt;Phase 4: Unlocking the Event-Driven Ecosystem&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jaiswalankit.com.np/The%20Evoluation%20of%20a%20SaaS/The%20Evolution%20of%20a%20SaaS%20-%207.png&quot; alt=&quot;The Async Microservice Shift&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Traffic Milestone: 100,000+ users.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;As your platform reaches enterprise-level utilization, you will likely offer advanced, resource-intensive features. Your application might generate comprehensive data exports, encode high-resolution media, or orchestrate complex interactions with external Artificial Intelligence models. &lt;/p&gt;
&lt;p&gt;Handling these monumental tasks synchronously forcing the user to wait while the main API processes the data monopolizes server threads and impacts the experience of other concurrent users. This is the perfect moment to evolve your architecture by isolating these specific workloads into independent &lt;strong&gt;Microservices&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jaiswalankit.com.np/The%20Evoluation%20of%20a%20SaaS/The%20Evolution%20of%20a%20SaaS%20-%208.png&quot; alt=&quot;Event-Driven Scaling&quot;&gt;&lt;/p&gt;
&lt;p&gt;Instead of processing heavy tasks on the spot, the main application adopts an &lt;strong&gt;Asynchronous, Event-Driven Architecture&lt;/strong&gt;. The platform delegates the heavy lifting by utilizing a highly durable &lt;strong&gt;Message Broker&lt;/strong&gt; (such as Apache Kafka, Amazon SQS, or RabbitMQ).&lt;/p&gt;
&lt;p&gt;The interaction becomes a seamless, non-blocking flow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The API Gateway:&lt;/strong&gt; The user requests a complex AI generation. The main API immediately returns a &lt;code&gt;202 Accepted&lt;/code&gt; response, assuring the user that the process has begun.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Event Stream:&lt;/strong&gt; The API securely publishes a message payload containing the job details into a dedicated queue. &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Worker Fleet:&lt;/strong&gt; Independent, specialized microservices designed specifically for CPU-intensive or GPU-intensive workloads listen to this queue. They pick up the messages and process the tasks silently in the background, updating the main database or notifying the user via WebSockets upon completion.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This architectural leap provides the ultimate capability: &lt;strong&gt;Granular, Event-Driven Scaling&lt;/strong&gt;. Because your services are decoupled, you can optimize your infrastructure costs brilliantly. If there is a massive backlog of video encoding jobs, you can configure your cloud environment to automatically spin up 50 instances of the specific &quot;Video Worker&quot; microservice to drain the queue quickly, while keeping your main API fleet operating at a stable, cost-effective baseline.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;cultivating-an-ecosystem-of-continuous-learning&quot;&gt;Cultivating an Ecosystem of Continuous Learning&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jaiswalankit.com.np/The%20Evoluation%20of%20a%20SaaS/The%20Evolution%20of%20a%20SaaS%20-%209.png&quot; alt=&quot;Grow Your Tech&quot;&gt;&lt;/p&gt;
&lt;p&gt;The evolution from a single server to a distributed microservices architecture is a fascinating journey of continuous technical improvement. The most effective engineering teams understand that architecture is never truly &quot;finished&quot;; it is a living ecosystem that adapts to serve the user.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Start with a Modular Monolith&lt;/strong&gt; to maximize learning, validate your market hypothesis, and establish clean domain boundaries.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Introduce Load Balancing and Statelessness&lt;/strong&gt; to guarantee high availability and consistent performance as your audience grows.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Optimize Data Retrieval&lt;/strong&gt; through intelligent memory caching and database replication, ensuring your persistence layer is always responsive.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Embrace Event-Driven Microservices&lt;/strong&gt; strategically, isolating complex workloads to unlock specialized computing power and granular scalability.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To guide this evolution effectively, implementing deep &lt;strong&gt;Observability&lt;/strong&gt; is key. By integrating tools like OpenTelemetry, Prometheus, and distributed tracing, you grant your engineering team the ability to visualize how data flows through the system. These metrics serve as the guiding light, telling you exactly when and where to apply the next architectural evolution. &lt;/p&gt;
&lt;p&gt;Build systems that serve the present beautifully, while actively designing the foundational agility required for the innovations of tomorrow.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jaiswalankit.com.np/The%20Evoluation%20of%20a%20SaaS/The%20Evolution%20of%20a%20SaaS%20-%2010.png&quot; alt=&quot;Let's Connect&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Looking to strategically evolve your platform's architecture? Let's Connect!&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;I am a Senior Full Stack AI Engineer specializing in the design, deployment, and optimization of highly resilient, cloud-agnostic SaaS platforms and intelligent, event-driven applications.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16467/the-evolution-of-a-saas-architecture</guid>
<pubDate>Sun, 03 May 2026 04:04:26 +0000</pubDate>
</item>
<item>
<title>Designing Audit-Proof Financial Workflows with Immutable Logs</title>
<link>https://coderlegion.com/15054/designing-audit-proof-financial-workflows-with-immutable-logs</link>
<description>&lt;hr&gt;
&lt;p&gt;In financial software, data does not just need to be correct — it needs to be provably correct. Regulators, auditors, and compliance teams do not take your word for it. They need an unbroken chain of evidence showing exactly what happened, when it happened, and who triggered it.&lt;/p&gt;
&lt;p&gt;I have built financial platforms where a missing audit trail meant weeks of manual reconciliation. I have also built systems where every state change was captured in an append-only log that could answer any auditor's question in seconds. The difference is not complexity — it is intentional design.&lt;/p&gt;
&lt;p&gt;This article covers how to build audit-proof financial workflows using immutable event logs, covering architecture decisions, implementation patterns in both Laravel and Node.js, and the operational practices that make the system trustworthy under scrutiny.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-mutable-records-break-under-audit&quot;&gt;Why Mutable Records Break Under Audit&lt;/h3&gt;
&lt;p&gt;Most web applications store the current state of a record. When an invoice status changes from &quot;pending&quot; to &quot;paid,&quot; the database row is updated in place. The previous state is gone.&lt;/p&gt;
&lt;p&gt;This works fine for most applications. For financial systems, it is a liability:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No history:&lt;/strong&gt; You cannot prove what the invoice status was yesterday at 3pm.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No attribution:&lt;/strong&gt; You cannot tell who changed the status or why.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No reconciliation path:&lt;/strong&gt; When numbers do not match, there is no trail to follow.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No tamper evidence:&lt;/strong&gt; A modified record looks identical to a legitimate one.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Financial regulations — PCI DSS, SOX, FCA guidelines — require that systems maintain a complete, tamper-evident record of all financial transactions and state changes. An UPDATE statement destroys exactly the evidence you are required to preserve.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-immutable-log-pattern&quot;&gt;The Immutable Log Pattern&lt;/h3&gt;
&lt;p&gt;The solution is to never update or delete financial records. Instead, every change is recorded as a new, append-only event:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[Event 1] Invoice #1042 CREATED — amount: £5,000 — by: user_42 — at: 2025-03-01T09:15:00Z
[Event 2] Invoice #1042 APPROVED — approved_by: user_15 — at: 2025-03-01T14:30:00Z
[Event 3] Invoice #1042 PAYMENT_RECEIVED — amount: £5,000 — ref: PAY_8821 — at: 2025-03-05T11:00:00Z
[Event 4] Invoice #1042 RECONCILED — matched_to: BANK_TXN_4401 — at: 2025-03-06T08:45:00Z
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The current state of the invoice is derived by replaying the events in order. The audit trail is the events themselves. Nothing is lost, nothing is overwritten.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;architecture-overview&quot;&gt;Architecture Overview&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[Client Request]
       |
       v
[Application Layer]
  - Validate business rules
  - Create event record
       |
       v
[DB Transaction]
  - Write event to audit_events table (append-only)
  - Update materialised view / current-state table
       |
       v
[Audit Events Table]           [Current State Table]
  (immutable, append-only)      (derived, rebuildable)
       |
       v
[Async Consumers]
  - Compliance reporting
  - Reconciliation engine
  - Notification service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The critical design decision: the audit events table is the source of truth. The current-state table is a projection that can be rebuilt from events at any time.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;implementation-in-laravel&quot;&gt;Implementation in Laravel&lt;/h3&gt;
&lt;h4 id=&quot;audit-events-migration&quot;&gt;Audit Events Migration&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;Schema::create('audit_events', function (Blueprint $table) {
    $table-&amp;gt;uuid('id')-&amp;gt;primary();
    $table-&amp;gt;string('aggregate_type');
    $table-&amp;gt;string('aggregate_id');
    $table-&amp;gt;unsignedBigInteger('sequence_number');
    $table-&amp;gt;string('event_type');
    $table-&amp;gt;json('payload');
    $table-&amp;gt;json('metadata');
    $table-&amp;gt;string('actor_id');
    $table-&amp;gt;string('actor_type');
    $table-&amp;gt;string('ip_address')-&amp;gt;nullable();
    $table-&amp;gt;string('checksum');
    $table-&amp;gt;timestamp('occurred_at');
    $table-&amp;gt;timestamp('created_at');

    $table-&amp;gt;unique(['aggregate_type', 'aggregate_id', 'sequence_number']);
    $table-&amp;gt;index(['aggregate_type', 'aggregate_id']);
    $table-&amp;gt;index('event_type');
    $table-&amp;gt;index('occurred_at');
    $table-&amp;gt;index('actor_id');
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;checksum&lt;/code&gt; column stores a hash of the event data plus the previous event's checksum, forming a hash chain. Any tampering with a historical record breaks the chain.&lt;/p&gt;
&lt;h4 id=&quot;audit-event-service&quot;&gt;Audit Event Service&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;namespace App\Services;

use App\Models\AuditEvent;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

class AuditEventService
{
    public function record(
        string $aggregateType,
        string $aggregateId,
        string $eventType,
        array $payload,
        string $actorId,
        string $actorType = 'user',
        ?string $ipAddress = null,
    ): AuditEvent {
        return DB::transaction(function () use (
            $aggregateType, $aggregateId, $eventType,
            $payload, $actorId, $actorType, $ipAddress
        ) {
            $lastEvent = AuditEvent::where('aggregate_type', $aggregateType)
                -&amp;gt;where('aggregate_id', $aggregateId)
                -&amp;gt;orderByDesc('sequence_number')
                -&amp;gt;lockForUpdate()
                -&amp;gt;first();

            $sequenceNumber = $lastEvent ? $lastEvent-&amp;gt;sequence_number + 1 : 1;
            $previousChecksum = $lastEvent?-&amp;gt;checksum ?? 'GENESIS';

            $eventData = [
                'aggregate_type' =&amp;gt; $aggregateType,
                'aggregate_id' =&amp;gt; $aggregateId,
                'sequence_number' =&amp;gt; $sequenceNumber,
                'event_type' =&amp;gt; $eventType,
                'payload' =&amp;gt; $payload,
                'actor_id' =&amp;gt; $actorId,
                'occurred_at' =&amp;gt; now(),
            ];

            $checksum = hash('sha256', $previousChecksum . json_encode($eventData));

            return AuditEvent::create([
                'id' =&amp;gt; Str::uuid(),
                ...$eventData,
                'metadata' =&amp;gt; [
                    'previous_checksum' =&amp;gt; $previousChecksum,
                    'schema_version' =&amp;gt; '1.0',
                ],
                'actor_type' =&amp;gt; $actorType,
                'ip_address' =&amp;gt; $ipAddress,
                'checksum' =&amp;gt; $checksum,
                'created_at' =&amp;gt; now(),
            ]);
        });
    }

    public function verifyChain(string $aggregateType, string $aggregateId): bool
    {
        $events = AuditEvent::where('aggregate_type', $aggregateType)
            -&amp;gt;where('aggregate_id', $aggregateId)
            -&amp;gt;orderBy('sequence_number')
            -&amp;gt;get();

        $previousChecksum = 'GENESIS';

        foreach ($events as $event) {
            $eventData = [
                'aggregate_type' =&amp;gt; $event-&amp;gt;aggregate_type,
                'aggregate_id' =&amp;gt; $event-&amp;gt;aggregate_id,
                'sequence_number' =&amp;gt; $event-&amp;gt;sequence_number,
                'event_type' =&amp;gt; $event-&amp;gt;event_type,
                'payload' =&amp;gt; $event-&amp;gt;payload,
                'actor_id' =&amp;gt; $event-&amp;gt;actor_id,
                'occurred_at' =&amp;gt; $event-&amp;gt;occurred_at,
            ];

            $expectedChecksum = hash('sha256', $previousChecksum . json_encode($eventData));

            if ($expectedChecksum !== $event-&amp;gt;checksum) {
                return false;
            }

            $previousChecksum = $event-&amp;gt;checksum;
        }

        return true;
    }

    public function getHistory(
        string $aggregateType,
        string $aggregateId
    ): \Illuminate\Support\Collection {
        return AuditEvent::where('aggregate_type', $aggregateType)
            -&amp;gt;where('aggregate_id', $aggregateId)
            -&amp;gt;orderBy('sequence_number')
            -&amp;gt;get();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;using-it-in-a-financial-workflow&quot;&gt;Using It in a Financial Workflow&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class InvoiceService
{
    public function __construct(
        private AuditEventService $auditService,
    ) {}

    public function createInvoice(array $data, string $userId, string $ip): Invoice
    {
        return DB::transaction(function () use ($data, $userId, $ip) {
            $invoice = Invoice::create([
                'number' =&amp;gt; $this-&amp;gt;generateInvoiceNumber(),
                'client_id' =&amp;gt; $data['client_id'],
                'amount' =&amp;gt; $data['amount'],
                'currency' =&amp;gt; $data['currency'],
                'status' =&amp;gt; 'draft',
                'due_date' =&amp;gt; $data['due_date'],
            ]);

            $this-&amp;gt;auditService-&amp;gt;record(
                aggregateType: 'invoice',
                aggregateId: (string) $invoice-&amp;gt;id,
                eventType: 'invoice.created',
                payload: [
                    'number' =&amp;gt; $invoice-&amp;gt;number,
                    'amount' =&amp;gt; $invoice-&amp;gt;amount,
                    'currency' =&amp;gt; $invoice-&amp;gt;currency,
                    'client_id' =&amp;gt; $invoice-&amp;gt;client_id,
                ],
                actorId: $userId,
                ipAddress: $ip,
            );

            return $invoice;
        });
    }

    public function approveInvoice(Invoice $invoice, string $approverId, string $ip): Invoice
    {
        return DB::transaction(function () use ($invoice, $approverId, $ip) {
            $invoice-&amp;gt;update(['status' =&amp;gt; 'approved']);

            $this-&amp;gt;auditService-&amp;gt;record(
                aggregateType: 'invoice',
                aggregateId: (string) $invoice-&amp;gt;id,
                eventType: 'invoice.approved',
                payload: [
                    'previous_status' =&amp;gt; 'draft',
                    'new_status' =&amp;gt; 'approved',
                ],
                actorId: $approverId,
                ipAddress: $ip,
            );

            return $invoice-&amp;gt;fresh();
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;implementation-in-node-js&quot;&gt;Implementation in Node.js&lt;/h3&gt;
&lt;h4 id=&quot;prisma-schema&quot;&gt;Prisma Schema&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-prisma&quot;&gt;model AuditEvent {
  id              String   @id @default(uuid())
  aggregateType   String   @map(&quot;aggregate_type&quot;)
  aggregateId     String   @map(&quot;aggregate_id&quot;)
  sequenceNumber  Int      @map(&quot;sequence_number&quot;)
  eventType       String   @map(&quot;event_type&quot;)
  payload         Json
  metadata        Json
  actorId         String   @map(&quot;actor_id&quot;)
  actorType       String   @map(&quot;actor_type&quot;) @default(&quot;user&quot;)
  ipAddress       String?  @map(&quot;ip_address&quot;)
  checksum        String
  occurredAt      DateTime @map(&quot;occurred_at&quot;)
  createdAt       DateTime @default(now()) @map(&quot;created_at&quot;)

  @@unique([aggregateType, aggregateId, sequenceNumber])
  @@index([aggregateType, aggregateId])
  @@index([eventType])
  @@index([occurredAt])
  @@index([actorId])
  @@map(&quot;audit_events&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;audit-event-service-1&quot;&gt;Audit Event Service&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { PrismaClient } from '@prisma/client';
import crypto from 'crypto';

const prisma = new PrismaClient();

interface RecordEventParams {
  aggregateType: string;
  aggregateId: string;
  eventType: string;
  payload: Record&amp;lt;string, unknown&amp;gt;;
  actorId: string;
  actorType?: string;
  ipAddress?: string;
}

export async function recordAuditEvent(params: RecordEventParams) {
  return prisma.$transaction(async (tx) =&amp;gt; {
    const lastEvent = await tx.auditEvent.findFirst({
      where: {
        aggregateType: params.aggregateType,
        aggregateId: params.aggregateId,
      },
      orderBy: { sequenceNumber: 'desc' },
    });

    const sequenceNumber = lastEvent ? lastEvent.sequenceNumber + 1 : 1;
    const previousChecksum = lastEvent?.checksum ?? 'GENESIS';

    const eventData = {
      aggregate_type: params.aggregateType,
      aggregate_id: params.aggregateId,
      sequence_number: sequenceNumber,
      event_type: params.eventType,
      payload: params.payload,
      actor_id: params.actorId,
      occurred_at: new Date().toISOString(),
    };

    const checksum = crypto
      .createHash('sha256')
      .update(previousChecksum + JSON.stringify(eventData))
      .digest('hex');

    return tx.auditEvent.create({
      data: {
        aggregateType: params.aggregateType,
        aggregateId: params.aggregateId,
        sequenceNumber,
        eventType: params.eventType,
        payload: params.payload,
        metadata: {
          previous_checksum: previousChecksum,
          schema_version: '1.0',
        },
        actorId: params.actorId,
        actorType: params.actorType ?? 'user',
        ipAddress: params.ipAddress,
        checksum,
        occurredAt: new Date(),
      },
    });
  });
}

export async function verifyAuditChain(
  aggregateType: string,
  aggregateId: string
): Promise&amp;lt;boolean&amp;gt; {
  const events = await prisma.auditEvent.findMany({
    where: { aggregateType, aggregateId },
    orderBy: { sequenceNumber: 'asc' },
  });

  let previousChecksum = 'GENESIS';

  for (const event of events) {
    const eventData = {
      aggregate_type: event.aggregateType,
      aggregate_id: event.aggregateId,
      sequence_number: event.sequenceNumber,
      event_type: event.eventType,
      payload: event.payload,
      actor_id: event.actorId,
      occurred_at: event.occurredAt.toISOString(),
    };

    const expectedChecksum = crypto
      .createHash('sha256')
      .update(previousChecksum + JSON.stringify(eventData))
      .digest('hex');

    if (expectedChecksum !== event.checksum) {
      return false;
    }

    previousChecksum = event.checksum;
  }

  return true;
}

export async function getAggregateHistory(
  aggregateType: string,
  aggregateId: string
) {
  return prisma.auditEvent.findMany({
    where: { aggregateType, aggregateId },
    orderBy: { sequenceNumber: 'asc' },
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;express-route-for-audit-queries&quot;&gt;Express Route for Audit Queries&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { Router } from 'express';

const router = Router();

router.get('/audit/:aggregateType/:aggregateId', async (req, res) =&amp;gt; {
  const { aggregateType, aggregateId } = req.params;

  const history = await getAggregateHistory(aggregateType, aggregateId);

  res.json({
    aggregate: { type: aggregateType, id: aggregateId },
    eventCount: history.length,
    events: history.map((e) =&amp;gt; ({
      sequence: e.sequenceNumber,
      type: e.eventType,
      payload: e.payload,
      actor: e.actorId,
      occurredAt: e.occurredAt,
    })),
  });
});

router.get('/audit/:aggregateType/:aggregateId/verify', async (req, res) =&amp;gt; {
  const { aggregateType, aggregateId } = req.params;
  const valid = await verifyAuditChain(aggregateType, aggregateId);

  res.json({
    aggregate: { type: aggregateType, id: aggregateId },
    chainValid: valid,
    verifiedAt: new Date().toISOString(),
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;retention-and-archival-strategy&quot;&gt;Retention and Archival Strategy&lt;/h3&gt;
&lt;p&gt;Audit logs grow fast. A system processing 10,000 transactions per day generates millions of events per year. You need a retention strategy:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hot storage (0–90 days):&lt;/strong&gt; Primary database. Full query capability. Used for day-to-day operations and recent audits.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Warm storage (90 days–2 years):&lt;/strong&gt; Move older events to a read-optimised store (e.g., partitioned table, read replica, or columnar database). Still queryable but not on the critical path.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cold storage (2+ years):&lt;/strong&gt; Archive to object storage (S3, GCS) in a structured format (Parquet, JSON Lines). Retained for regulatory compliance periods (typically 5–7 years for financial data).&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;laravel-archival-command&quot;&gt;Laravel Archival Command&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class ArchiveAuditEvents extends Command
{
    protected $signature = 'audit:archive {--older-than=90}';

    public function handle(): void
    {
        $cutoff = now()-&amp;gt;subDays((int) $this-&amp;gt;option('older-than'));

        $events = AuditEvent::where('occurred_at', '&amp;lt;', $cutoff)
            -&amp;gt;orderBy('occurred_at')
            -&amp;gt;cursor();

        $batch = [];
        $batchCount = 0;

        foreach ($events as $event) {
            $batch[] = $event-&amp;gt;toArray();

            if (count($batch) &amp;gt;= 10000) {
                $this-&amp;gt;uploadBatch($batch, $batchCount);
                $batch = [];
                $batchCount++;
            }
        }

        if (!empty($batch)) {
            $this-&amp;gt;uploadBatch($batch, $batchCount);
        }

        AuditEvent::where('occurred_at', '&amp;lt;', $cutoff)-&amp;gt;delete();

        $this-&amp;gt;info(&quot;Archived and purged events older than {$cutoff-&amp;gt;toDateString()}.&quot;);
    }

    private function uploadBatch(array $batch, int $index): void
    {
        $filename = sprintf('audit-archive/%s/batch-%04d.json', now()-&amp;gt;format('Y/m'), $index);

        Storage::disk('s3')-&amp;gt;put(
            $filename,
            collect($batch)-&amp;gt;map(fn ($e) =&amp;gt; json_encode($e))-&amp;gt;implode(&quot;\n&quot;)
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;reconciliation-with-immutable-logs&quot;&gt;Reconciliation with Immutable Logs&lt;/h3&gt;
&lt;p&gt;One of the most valuable outcomes of immutable logs is automated reconciliation. Instead of manually comparing spreadsheets, you can programmatically verify that every financial event has a matching counterpart:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class ReconciliationService
{
    public function reconcilePayments(string $date): array
    {
        $internalEvents = AuditEvent::where('event_type', 'payment.completed')
            -&amp;gt;whereDate('occurred_at', $date)
            -&amp;gt;get()
            -&amp;gt;keyBy(fn ($e) =&amp;gt; $e-&amp;gt;payload['gateway_reference']);

        $bankStatements = $this-&amp;gt;fetchBankStatements($date);

        $matched = [];
        $unmatchedInternal = [];
        $unmatchedBank = [];

        foreach ($bankStatements as $statement) {
            if ($internalEvents-&amp;gt;has($statement-&amp;gt;reference)) {
                $matched[] = $statement-&amp;gt;reference;
                $internalEvents-&amp;gt;forget($statement-&amp;gt;reference);
            } else {
                $unmatchedBank[] = $statement;
            }
        }

        $unmatchedInternal = $internalEvents-&amp;gt;values()-&amp;gt;all();

        return [
            'date' =&amp;gt; $date,
            'matched_count' =&amp;gt; count($matched),
            'unmatched_internal' =&amp;gt; count($unmatchedInternal),
            'unmatched_bank' =&amp;gt; count($unmatchedBank),
            'discrepancies' =&amp;gt; [...$unmatchedInternal, ...$unmatchedBank],
        ];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;production-results&quot;&gt;Production Results&lt;/h3&gt;
&lt;p&gt;After implementing immutable audit logging across financial platforms:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Metric &lt;/th&gt;&lt;th&gt;Before &lt;/th&gt;&lt;th&gt;After&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Reconciliation time (weekly) &lt;/td&gt;&lt;td&gt;4+ hours manual &lt;/td&gt;&lt;td&gt;15 minutes automated&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Audit preparation time &lt;/td&gt;&lt;td&gt;2–3 weeks &lt;/td&gt;&lt;td&gt;2 days&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Unresolved discrepancies &lt;/td&gt;&lt;td&gt;8–12 per month &lt;/td&gt;&lt;td&gt;0–1 per month&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Compliance audit findings &lt;/td&gt;&lt;td&gt;Multiple per review &lt;/td&gt;&lt;td&gt;Zero material findings&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Data dispute resolution time &lt;/td&gt;&lt;td&gt;3–5 days &lt;/td&gt;&lt;td&gt;&amp;lt; 1 hour&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;key-takeaways&quot;&gt;Key Takeaways&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Never update financial records in place.&lt;/strong&gt; Append new events instead. The current state is a projection of the event history.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use hash chains for tamper evidence.&lt;/strong&gt; Each event's checksum includes the previous event's checksum. Any modification breaks the chain.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Record who, what, when, and why.&lt;/strong&gt; Every event needs an actor, a timestamp, and the business context. IP addresses and session identifiers add forensic value.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Plan for retention from day one.&lt;/strong&gt; Audit logs grow quickly. Tiered storage (hot/warm/cold) keeps costs manageable while satisfying compliance requirements.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automate reconciliation.&lt;/strong&gt; Immutable logs enable programmatic reconciliation that replaces hours of manual spreadsheet comparison.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Write events and business data in the same transaction.&lt;/strong&gt; If they are not atomic, you risk inconsistency between your source of truth and your audit trail.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion-1&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Audit-proof financial workflows are not about generating more logs — they are about designing systems where every state change is captured, chained, and verifiable. Append-only writes, hash-chain integrity, and tiered retention create a system that answers any auditor's question with evidence, not explanations.&lt;/p&gt;
&lt;p&gt;Whether you build in Laravel or Node.js, the patterns are the same. This infrastructure pays dividends every time a regulator asks a question or a discrepancy needs resolution.&lt;/p&gt;
&lt;hr&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/15054/designing-audit-proof-financial-workflows-with-immutable-logs</guid>
<pubDate>Fri, 17 Apr 2026 10:09:35 +0000</pubDate>
</item>
<item>
<title>From Delayed Data to Real-Time Decisions: Building an Event-Driven Pipeline on AWS</title>
<link>https://coderlegion.com/13228/from-delayed-data-to-real-time-decisions-building-an-event-driven-pipeline-on-aws</link>
<description>&lt;p&gt;At 2:17 AM, the dashboard wasn’t moving.&lt;/p&gt;
&lt;p&gt;Transactions were still coming in, but the numbers on the screen were frozen. The system was running, but the data was already 10 minutes behind.&lt;/p&gt;
&lt;p&gt;At that scale, 10 minutes wasn’t just delay — it meant decisions based on outdated information.&lt;/p&gt;
&lt;p&gt;That was the moment everything had to change.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Problem: When Data Arrives Too Late&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In many systems, data pipelines are built around batch processing or synchronous APIs. They work — until they don’t.&lt;/p&gt;
&lt;p&gt;As traffic grows, these systems start to break in predictable ways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Increasing latency&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Bottlenecks in database reads&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Inconsistent data across services&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Delayed analytics and reporting&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The core issue is simple:&lt;br&gt;
the system reacts too late.&lt;/p&gt;
&lt;p&gt;To solve this, the architecture had to shift from request-driven to event-driven.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Shift: Thinking in Events&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Instead of asking for data, services react to events as they happen.&lt;/p&gt;
&lt;p&gt;Every transaction becomes an event. Every state change is recorded as a fact.&lt;/p&gt;
&lt;p&gt;This changes everything:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Systems become asynchronous&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Components are loosely coupled&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Scalability comes from partitioning, not vertical scaling&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The goal was clear:&lt;/p&gt;
&lt;p&gt;Process more than 15,000 events per second with sub-50ms latency — reliably.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Designing the Pipeline&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The first version of the system was built as a distributed architecture using open-source tools.&lt;/p&gt;
&lt;p&gt;Core components:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- Apache Kafka for event streaming

- Kafka Streams for real-time processing

- Spring Boot for processing services

- PostgreSQL for durable storage

- Redis for low-latency reads

- Prometheus and Grafana for observability
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Event Flow&lt;/p&gt;
&lt;p&gt;The pipeline follows a simple but powerful structure:&lt;/p&gt;
&lt;p&gt;Producer → Kafka → Stream Processing → Storage → Analytics&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Producers publish transaction events&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Events are serialized and validated&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Kafka distributes them across partitions&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stream processors handle transformations and aggregations&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Results are stored and made available for real-time queries&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This allowed continuous processing instead of waiting for batches.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Challenges (Where Systems Usually Break)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Designing the pipeline was only part of the work. Making it reliable at scale required solving real issues:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Event Duplication&lt;br&gt;
Ensuring exactly-once processing required transactional guarantees and careful handling of offsets.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Latency Spikes&lt;br&gt;
Under heavy load, consumer lag increased. This was mitigated with parallel consumers and optimized batching.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Throughput Optimization&lt;br&gt;
Producer batching (32KB) and Snappy compression significantly improved performance.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Resource Bottlenecks&lt;br&gt;
Connection pooling and efficient serialization reduced pressure on downstream systems.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These are the kinds of problems that don’t show up in diagrams — only in real systems.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Results&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;After multiple iterations and optimizations, the system achieved:&lt;/p&gt;
&lt;p&gt;Throughput: 15,000+ events per second&lt;/p&gt;
&lt;p&gt;Latency (P99): under 50ms&lt;/p&gt;
&lt;p&gt;Availability: 99.95%&lt;/p&gt;
&lt;p&gt;Data loss: 0% (exactly-once processing)&lt;/p&gt;
&lt;p&gt;More importantly, this enabled near real-time anomaly detection.&lt;/p&gt;
&lt;p&gt;What used to take minutes could now happen in milliseconds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Moving to AWS: Same Architecture, Less Infrastructure&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Once the system was stable, the next step was moving to the cloud.&lt;/p&gt;
&lt;p&gt;The key decision was not to redesign the system — only to replace infrastructure with managed services.&lt;/p&gt;
&lt;p&gt;Cloud Architecture: &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Event ingestion via Amazon EventBridge or Amazon MSK&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Processing with AWS Lambda&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Workflow orchestration using AWS Step Functions&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Storage with DynamoDB or Amazon RDS&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Monitoring through Amazon CloudWatch&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each component maps directly to the original architecture.&lt;/p&gt;
&lt;p&gt;The logic stays the same. The operations overhead disappears.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Key Insight&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The most important lesson wasn’t about tools.&lt;/p&gt;
&lt;p&gt;It was about understanding the system.&lt;/p&gt;
&lt;p&gt;Event-driven architectures are built on a few core principles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Events are immutable&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Systems react asynchronously&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Scalability comes from partitioned streams&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;State is derived from event logs&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once these are clear, moving between technologies becomes straightforward.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Final Thoughts&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Real-time systems are not about speed alone.&lt;/p&gt;
&lt;p&gt;They are about enabling better decisions.&lt;/p&gt;
&lt;p&gt;When data arrives late, the system might still work — but the business doesn’t.&lt;/p&gt;
&lt;p&gt;Designing event-driven pipelines is ultimately about reducing the gap between what happens and what the system knows.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Design, therefore I exist.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13228/from-delayed-data-to-real-time-decisions-building-an-event-driven-pipeline-on-aws</guid>
<pubDate>Thu, 19 Mar 2026 21:11:10 +0000</pubDate>
</item>
<item>
<title>Schema-First Approach with ThingsDB</title>
<link>https://coderlegion.com/8421/schema-first-approach-with-thingsdb</link>
<description>&lt;p&gt;In many architectures, the separation between database and application code introduces a dangerous gap where data integrity can fail. The application code is responsible for validation, but what if multiple services access the same data?&lt;/p&gt;
&lt;p&gt;This post explores a different approach. We'll show you how to leverage ThingsDB's core features—&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.thingsdb.io/v1/overview/collections/#typed-collection&quot;&gt;Typed Collections&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.thingsdb.io/v1/procedures-api/&quot;&gt;Procedures&lt;/a&gt;, and &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.thingsdb.io/v1/data-types/room/&quot;&gt;Rooms&lt;/a&gt;—to make the database itself the enforcing authority.&lt;/p&gt;
&lt;p&gt;Follow along as we set up a secure, event-driven API for an &lt;code&gt;Order&lt;/code&gt; application, demonstrating how to lock down access and ensure every data change is both valid and consistent.&lt;/p&gt;
&lt;h3 id=&quot;1-setting-the-stage-the-blank-slate&quot;&gt;1. Setting the Stage: The Blank Slate&lt;/h3&gt;
&lt;p&gt;We begin by creating a fresh collection for our application.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(execute the code snippet in the &lt;code&gt;/thingsdb&lt;/code&gt; scope)&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;new_collection('OrderApp');
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;2-understanding-typed-collections-the-core-defense-&quot;&gt;2. Understanding Typed Collections (The Core Defense)&lt;/h3&gt;
&lt;p&gt;The first step toward protection is establishing a &lt;strong&gt;schema&lt;/strong&gt;. By default, a new ThingsDB collection starts with an empty, unrestricted object (a basic &quot;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.thingsdb.io/v1/data-types/thing/&quot;&gt;thing&lt;/a&gt;&quot;) at its root. Any user with write access can add &lt;em&gt;anything&lt;/em&gt; to it.&lt;/p&gt;
&lt;p&gt;We want to lock this down. We create a type, &lt;code&gt;App&lt;/code&gt;, to serve as the structured blueprint for the collection's entire existence.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(execute the code snippet in the &lt;code&gt;//OrderApp&lt;/code&gt; scope)&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 1. Create a type for the root of my collection
new_type('App');

// 2. Convert the collection root to the 'App' type
.to_type('App');
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; The collection is now restricted by the &lt;code&gt;App&lt;/code&gt; type. No data can be added or modified unless the modification conforms to changes made to the &lt;code&gt;App&lt;/code&gt; type definition. This is your primary schema defense.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;3-defining-the-data-model&quot;&gt;3. Defining the Data Model&lt;/h3&gt;
&lt;p&gt;Next, we define the structure of the data we want to store—the &lt;code&gt;Order&lt;/code&gt; object—and where to keep it within the &lt;code&gt;App&lt;/code&gt; root.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(execute the code snippet in the &lt;code&gt;//OrderApp&lt;/code&gt; scope)&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Create an enumerator for consistent order status values
set_enum('OrderStatus', {
    OPEN: 'Open',
    PENDING: 'Pending',
    CLOSED: 'Closed',
});

// Define the Order structure
set_type('Order', {
    id: '#',               // Return ID as `id` when wrapped
    name: 'str',
    price: 'float',
    status: 'OrderStatus', // Enforcing status consistency
    created: 'datetime',
});

// Update the App type: Add a property 'orders' as an array of Order objects
mod_type('App', 'add', 'orders', '[Order]');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;mod_type(..)&lt;/code&gt; call automatically creates the &lt;code&gt;orders&lt;/code&gt; property as an empty list on the collection root, ready to hold our data.&lt;/p&gt;
&lt;h3 id=&quot;4-establishing-an-event-channel&quot;&gt;4. Establishing an Event Channel&lt;/h3&gt;
&lt;p&gt;For a modern microservice or component-based application, polling for data changes is inefficient. We set up an event channel using a &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.thingsdb.io/v1/data-types/room/&quot;&gt;Room&lt;/a&gt; to enable real-time communication.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(execute the code snippet in the &lt;code&gt;//OrderApp&lt;/code&gt; scope)&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Add a room property to the App type for event handling
mod_type('App', 'add', 'order_events', 'room');

// For easy identification, we give the room a fixed name
.order_events.set_name('api_order_events');
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Benefit:&lt;/strong&gt; Any component can now join this room and instantly react to events like &lt;code&gt;new-order&lt;/code&gt; or &lt;code&gt;change-order-status&lt;/code&gt; without needing to constantly query the collection.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;5-encapsulating-logic-with-procedures-the-api-layer-&quot;&gt;5. Encapsulating Logic with Procedures (The API Layer)&lt;/h3&gt;
&lt;p&gt;To guarantee that only valid changes are made to the collection, we expose all actions &lt;em&gt;only&lt;/em&gt; through &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.thingsdb.io/v1/procedures-api/&quot;&gt;Procedures&lt;/a&gt;. We prefix these procedures with &lt;code&gt;api_&lt;/code&gt; for easy security whitelisting later.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(execute the code snippet in the &lt;code&gt;//OrderApp&lt;/code&gt; scope)&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 1. Add a new order (with validation on input types)
new_procedure('api_order_add', |name, price| {
    order = Order{
        name:,
        price:,
    };
    .orders.push(order);

    // Emit the 'new-order' event for all listeners
    .order_events.emit('new-order', order.wrap());
    nil;
});

// 2. Change order status
new_procedure('api_order_set_status', |order_id, status| {
    // Input validation: ensures the new status is a valid enum value
    new_status = OrderStatus{||status.upper()};

    // Access and update the specific Order object
    Order(order_id).status = new_status;

    // Emit the 'change-order-status' event
    .order_events.emit('change-order-status', order_id, new_status.value());
    nil;
});

// 3. Retrieve orders by status
new_procedure('api_orders_by_status', |status| {
    order_status = OrderStatus{||status.upper()};
    .orders.filter(|order| order.status == order_status).map_wrap();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;6-strict-security-the-locked-down-api-user&quot;&gt;6. Strict Security: The Locked-Down API User&lt;/h3&gt;
&lt;p&gt;The final and most crucial step is to create a user that can &lt;strong&gt;only execute these specific procedures&lt;/strong&gt;. This prevents any external service from bypassing your defined business logic.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(execute the code snippet in the &lt;code&gt;/thingsdb&lt;/code&gt; scope)&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Create a new user for microservice access
new_user('api');

// Grant access to the collection:
// CHANGE (to modify data), JOIN (to listen to events), RUN (to execute procedures)
grant('//OrderApi', 'api', CHANGE|JOIN|RUN);

// Whitelist: This is the security lock.
// The user can only execute procedures and join rooms starting with 'api_'
whitelist_add('api', 'procedures', /^api_.*/);
whitelist_add('api', 'rooms', /^api_.*/);

// Generate and return the authentication token
new_token('api');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this setup, the &lt;code&gt;api&lt;/code&gt; user is completely restricted. It cannot arbitrarily delete the collection, change its structure, or modify data without going through the validated input and logic defined in your &lt;code&gt;api_&lt;/code&gt; procedures. Your collection is now truly protected by code!&lt;/p&gt;
&lt;p&gt;In the next post, we will switch gears to the client side. We will demonstrate how a microservice uses the generated token to connect to the &lt;code&gt;OrderApp&lt;/code&gt; collection, how to perform the authenticated &lt;code&gt;api_&lt;/code&gt; queries, and, crucially, how to &quot;listen&quot; to the real-time event changes emitted through the &lt;code&gt;api_order_events&lt;/code&gt; room.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/8421/schema-first-approach-with-thingsdb</guid>
<pubDate>Fri, 12 Dec 2025 06:48:09 +0000</pubDate>
</item>
</channel>
</rss>