Umur Inan/ Books/ Event-Driven Architecture with Spring Boot 4.x and Kafka 4.x/ Free Sample
Free sample. This is the Preface and Chapter 1 of Event-Driven Architecture with Spring Boot 4.x and Kafka 4.x by Umur Inan. The full book is available below.

Preface

I didn’t set out to write a book about event-driven architecture. I set out to build a system that wouldn’t fall over.

Like most engineers, I started with what I knew: REST APIs, shared databases, synchronous calls between services. It worked until it didn’t. The breaking point wasn’t dramatic. It was a series of small failures that compounded: a payment service timing out and taking the order service with it, a database becoming a bottleneck because three different services were reading and writing to the same tables, a deployment that required coordinating four teams because everything was too tightly coupled to release independently.

The answer, I kept reading, was event-driven architecture. Decouple your services. Use Kafka. Implement CQRS. Apply the Saga pattern.

The advice was everywhere. The clear, practical explanation of how, with real code, real trade-offs, and real failure modes, was much harder to find.

This book is my attempt to fill that gap.

With gratitude to my aunts Ayse Inan and Aynur Inan, who make everything possible.


What This Book Is

This is a hands-on guide to building event-driven systems with Apache Kafka 4.x and Spring Boot 4. Every chapter covers a single concept: CQRS, Event Sourcing, the Saga pattern, the Outbox pattern, and more. Each concept is paired with a complete, runnable Spring Boot project you can clone and run today.

The goal is not to convince you that event-driven architecture is always the right answer. It isn’t. The goal is to make sure that when it is the right answer, you have the tools and understanding to implement it correctly.


Who This Book Is For

This book assumes you already know Spring Boot basics. You’ve built a REST API, you understand dependency injection, you’re comfortable with Java. You don’t need to know Kafka, distributed systems theory, or any of the patterns covered here. We start from the beginning on all of that.

If you’re moving a monolith toward microservices, building a new distributed system from scratch, or trying to understand what all those architecture buzzwords actually mean in practice, this book is for you.


How to Use This Book

Each chapter is independent. If you already understand Kafka internals, skip Chapter 2. If CQRS is what brought you here, jump to Chapter 4. The chapters are designed so you can read them in any order without losing context.

That said, if you’re new to event-driven architecture, I recommend reading Chapters 1 through 7 in order. Each builds on the previous. Chapters 8 through 16 are largely independent and can be read in any order based on your immediate needs.


A Note on Kafka 4.x

All examples in this book use Apache Kafka 4.x with KRaft mode. KRaft replaces ZooKeeper entirely. Kafka now manages its own metadata internally. This means simpler deployments, faster startup, and one fewer system to operate.

If you’re working with an older Kafka cluster (2.x or earlier 3.x), the application-level code in every chapter is compatible. The Docker Compose files will need minor adjustments to add ZooKeeper.


A Note on Spring Boot 4

Spring Boot 4 requires Java 21 and brings significant improvements to observability, native compilation, and Testcontainers integration. All examples in this book are written for Spring Boot 4 and take advantage of its latest features where relevant.


Acknowledgments

The bugs are mine. The good ideas are everyone else’s, often without attribution because I forgot where I first heard them. If you recognize one of yours, please write to me; I would like to credit you in the next edition.


1 Why Event-Driven Architecture?

1.1 Overview

“A distributed system is one in which the failure of a computer you didn’t even know existed can render your own computer unusable.” Leslie Lamport


This is one of those stories you hear a lot in distributed systems circles. A fast-growing e-commerce company had a solid Black Friday plan: scale up the payment service to handle the traffic spike. They did. It worked. Then their inventory service, which nobody had thought to scale, started struggling under thousands of concurrent stock checks.

Within minutes, the payment service started timing out waiting for inventory responses. Order confirmations stopped going out. The entire checkout flow went down. One service’s bad day had become everyone’s worst day.

The team hadn’t done anything obviously wrong. They’d followed the standard playbook: split the monolith into microservices, connect them with REST calls. The problem wasn’t the split. It was how they connected the pieces.

Read on.

Tip

The examples in this chapter are conceptual. No runnable code yet. Starting from Chapter 2, each chapter includes Docker Compose files you can run locally.


By the end of this chapter, you’ll understand: - Why monoliths break down at scale, and why that’s not the monolith’s fault - Why synchronous microservices often make things worse, not better - What “thinking in events” actually means in practice - When event-driven architecture solves real problems, and when it creates new ones - What separates EDA from messaging and reactive programming


1.2 The Monolith: A Love Story

The monolith gets a bad reputation it doesn’t deserve.

When you’re building an e-commerce platform from scratch, a monolith is the right call. One codebase, one deployment, one database. Your OrderService calls your PaymentService as a method call, not a network request. No timeouts. No serialization. No service discovery. It either works or it throws an exception, and you find out immediately.

Here’s what a simple e-commerce monolith looks like:

Figure 1.1: The monolith: one JVM process, one transaction boundary
@Service
public class OrderService {

    private final PaymentService paymentService;
    private final InventoryService inventoryService; // (1)

    public Order placeOrder(OrderRequest request) {
        inventoryService.reserve(request.getItems()); // (2)
        paymentService.charge(request.getPayment());  // (3)
        return orderRepository.save(new Order(request));
    }
}

(1) Both services are Spring beans: no HTTP, no Kafka, just Java method calls

(2) Each call is synchronous and within the same transaction: if payment fails, inventory rolls back automatically

(3) Payment runs in the same transaction: a failure here rolls back both the payment and the inventory reservation

This is clean. This is simple. When something goes wrong, your stack trace tells you exactly where. Your database transaction handles consistency for free. You deploy one JAR and go home.

I’ve seen teams rewrite a perfectly good monolith into microservices and spend six months rebuilding functionality they used to take for granted: transactional consistency, integrated testing, a single place to search for logs. The monolith wasn’t the problem.

Scale was.

Tip

If you’re building a new product with a small team, start with a monolith. Event-driven architecture solves specific scaling and coupling problems. Don’t adopt it until you have those problems.

The monolith’s strength is also its weakness: everything runs in the same process. When your codebase grows, when your team grows, when your traffic grows, that single process becomes a liability. Understanding why is more important than just knowing that it does.

So what exactly breaks down?

1.3 When the Monolith Starts to Hurt

The monolith doesn’t fail dramatically. It degrades slowly, and then all at once.

There are three distinct failure modes. Each one feels manageable in isolation. Together, they make the monolith untenable at scale.

The scale problem. Every component in a monolith scales together. Your checkout flow is under heavy load? You scale up the entire application, including the reporting module nobody is using. Worse, a slow database query in one feature doesn’t just slow down that feature. It blocks a thread. Block enough threads and the entire application stalls.

When a thread blocks on a database call or an HTTP request, it holds its slot in the thread pool idle. It does nothing, but it also cannot be reused. Other incoming requests queue behind it. Once the pool is exhausted, the server stops accepting new work. It does not crash. It does not log an error. It just stops responding, and the only signal is a growing pile of timed-out requests in your monitoring.

This is one of the most common stories in distributed systems. A team adds a feature that generates a slow, expensive report. The report runs fine in isolation. Under load, it competes for database connections with the checkout flow. Black Friday arrives. Checkouts slow to a crawl, not because of anything wrong with the checkout code, but because a reporting query is holding a connection pool hostage.

The team problem. A monolith with four teams is four teams fighting over the same codebase. A change in the payment module requires a full application deployment. That deployment is now everyone’s concern: the team that touched payments, the team that didn’t, and the release manager who has to coordinate them all.

At a certain team size, the coordination cost of a monolith exceeds the coordination cost of distributed services. That threshold is lower than most people expect. Once you have more than two or three teams, a shared codebase starts creating more friction than it saves.

The reliability problem. In a monolith, one broken component can take down everything else. A memory leak in the notification module eventually crashes the JVM. The order service goes with it. Not because anything is wrong with orders, but because they share a process.

Warning

This is the failure mode that surprises teams most. The notification service failing and taking down checkout feels impossible until it happens to you.

The instinct at this point is correct: these services need to be independent. They shouldn’t share a process, a deployment, or a failure domain. The question is how to make them independent without creating a different set of problems.

The obvious answer is microservices. And it works, until it doesn’t.

Migration approach: The strangler fig pattern is the most practical path from monolith to event-driven microservices. Rather than rewriting everything at once, you extract one bounded context at a time. Route new traffic to the new service, leave existing traffic on the monolith until the new service proves stable, then cut over. Kafka makes this tractable: the new service can consume events from Debezium (CDC) on the legacy database without requiring any changes to the monolith itself.

So what goes wrong?

1.4 The Microservices Escape (And Its New Problems)

Splitting the monolith feels like the right move. And it is, if you do it carefully.

The natural approach is to extract services and connect them with REST calls. OrderService calls PaymentService over HTTP. PaymentService calls InventoryService. Each service has its own database, its own deployment, its own team. Independence achieved.

Except it isn’t.

// OrderService - the naive microservices approach
public Order placeOrder(OrderRequest request) {
    inventoryClient.reserve(request.getItems());  // (1)
    paymentClient.charge(request.getPayment());   // (1)
    return orderRepository.save(new Order(request));
}

(1) These are now HTTP calls: synchronous, blocking, and dependent on two external services being up and healthy

Figure 1.2: Synchronous REST microservices: each hop adds latency and creates a new point of failure

This looks almost identical to the monolith version. The difference is invisible until something goes wrong.

The cascading failure problem. When OrderService calls PaymentService synchronously, it waits. If PaymentService is slow, OrderService is slow. If PaymentService is down, OrderService is broken. You’ve distributed the deployment but kept the coupling. This is the distributed monolith: the worst of both worlds.

Think of it like a chain of phone calls. If everyone in the chain needs an answer before they can continue, one slow person halts the entire conversation. The only way to avoid that is to stop waiting for answers in real time.

The latency chain problem. Every synchronous hop adds latency. OrderService calls PaymentService (50ms) which calls FraudService (30ms) which calls a third-party API (200ms). Your order placement is now 280ms minimum, and that’s when everything is healthy. Add retries, add timeouts, and you can easily hit seconds.

The distributed transaction problem. In the monolith, a database transaction covered everything. In microservices with separate databases, there’s no such safety net. If InventoryService reserves stock and then PaymentService fails, you have reserved inventory for an order that never completed. Cleaning that up manually is painful. Preventing it is a design challenge that requires patterns we’ll spend several chapters on.

Going Deeper: The “distributed monolith” anti-pattern is described in depth in Sam Newman’s Building Microservices. If you’re mid-migration from a monolith and recognize this pattern in your system, that book is worth reading alongside this one.

Microservices with synchronous REST calls don’t remove coupling. They move it from compile-time to runtime and make it harder to see. The services are independently deployable in theory, but in practice, a failure in one still brings down others.

There’s a different way to think about this. What if services didn’t call each other at all?

1.5 Thinking in Events

The shift to event-driven architecture is less a technical change and more a change in how you think about communication between services.

In a synchronous system, a service calls another service. It says: “Do this. Wait for the result.” The caller and the callee are coupled in time. Both must be available simultaneously, and the caller blocks until the response arrives.

In an event-driven system, a service announces that something happened. It says: “This occurred.” Then it moves on. It doesn’t know who’s listening. It doesn’t wait for a response. It doesn’t care whether the downstream service is currently running.

This is the core idea. Everything else follows from it.

Events are facts, not requests. An event is something that happened, immutable and past tense. OrderPlaced. PaymentProcessed. InventoryReserved. You don’t send an OrderPlaced event and then change your mind. The order was placed. That’s a fact now. This immutability is what makes events a reliable foundation for distributed systems.

Producers don’t know consumers. When OrderService publishes an OrderPlaced event, it has no idea that PaymentService is listening. It also doesn’t know that NotificationService is listening to send a confirmation email, or that AnalyticsService is listening to track the conversion. You can add a new consumer without touching the producer at all.

This is true decoupling. Not just independent deployments, but independent existence.

The diagram below shows the difference:

Figure 1.3: REST vs event-driven: synchronous coupling versus asynchronous decoupling

In the REST model, OrderService has direct dependencies on both PaymentService and InventoryService. A failure in either affects OrderService directly.

In the event-driven model, OrderService publishes to a topic. PaymentService and InventoryService consume from it independently. OrderService doesn’t know they exist, and doesn’t need to.

Note

“Events” in this context means domain events: meaningful things that happened in your business domain. This is different from UI events, system events, or log entries, even though all of those are also called events. In this book, “event” always means a domain event.

The mental shift required here is real. We’re trained to think in terms of function calls: invoke this, get that back. Event-driven thinking inverts the control flow. Instead of asking “who do I need to call?”, you ask “what just happened, and who might care?”

That reframing changes the design of your entire system.

1.5.1 Who Owns What: Bounded Contexts and the Single-Writer Principle

This is the part most tutorials skip. It’s also the part that determines whether your event-driven system stays clean or turns into a distributed monolith dressed in Kafka topics.

A bounded context is a boundary within your domain where a specific model applies and a specific team has ownership. Inside that boundary, terms mean exactly what that team says they mean. Outside it, other teams have their own models, their own terms, their own data.

The rule that falls out of this: one service owns each entity type. Only the OrderService writes orders. Only the InventoryService writes inventory levels. Only the ProductCatalogService writes product data. This is the single-writer principle, and violating it is one of the fastest ways to create a nightmare.

When two services can both write the same entity, you get conflicts, split-brain state, and the kind of bugs that only show up at 2am when both services process the same event simultaneously and produce different answers. Ownership eliminates the ambiguity. There is exactly one place where an entity changes. Everything else learns about that change by consuming events.

Domain events vs. integration events is the distinction that makes ownership work at scale.

A domain event is internal to a service. It’s the raw language of the bounded context: Payment.CaptureSucceeded, Inventory.ReservationExpired, Order.LineItemAdded. These events reflect the internal state machine of the service. They use the service’s own terminology. They may contain internal IDs, internal status codes, fields that mean nothing outside.

An integration event is published across the boundary for other services to consume. OrderConfirmed, PaymentProcessed, ProductRestocked. These events are a deliberate, versioned contract. They use terms that other teams understand. They contain only what other teams need.

The difference is not just naming. It’s intent and audience.

Here is a concrete example. The OrderService processes a checkout flow. Internally, it goes through several state transitions:

Order.DraftCreated
Order.ItemsValidated
Order.PriceCalculated
Order.PaymentInitiated
Order.PaymentCaptured
Order.Confirmed

These are domain events. They’re the internal narrative of what happened inside OrderService. The PaymentService does not need to know that items were validated or that a price was calculated. That’s internal.

What PaymentService needs is the integration event: OrderConfirmed. Clean, stable, containing exactly the fields PaymentService cares about.

If you publish your internal domain events directly to shared Kafka topics, every consumer is now coupled to your internal model. When you rename a field, restructure your state machine, or add a step to your checkout flow, every consumer breaks. You’ve turned an internal refactor into a cross-team incident.

// Internal domain event - never publish this on a shared topic
record OrderPaymentCaptured(
    String orderId,
    String internalTransactionRef,   // internal ID, meaningless outside
    String paymentGatewayCode,       // internal status code
    BigDecimal capturedAmount,
    Instant capturedAt
) {}

// Integration event - this is what other services consume
record OrderConfirmed(
    String orderId,
    String customerId,
    BigDecimal totalAmount,
    Instant confirmedAt
) {}

The OrderService handles OrderPaymentCaptured internally, updates its own state, and then publishes OrderConfirmed when the order is fully confirmed. Other services see a clean, stable contract. The internal mechanics are hidden.

Warning

Publishing internal domain events on shared Kafka topics is a tight-coupling trap. You’ve replaced REST calls with event subscriptions, but consumers are still depending on your internal model. Any internal refactor requires coordinating changes with every consumer team. Keep internal events internal.

Tip

A practical test for whether an event should be internal or shared: can you rename its fields without telling another team? If yes, it’s internal. If no, it needs to be a versioned integration event with a stable schema.

This ownership model gives you the decoupling EDA promises. Services can evolve their internal models freely. They publish integration events when something of consequence happens. Other services consume those events and build their own projections. Nobody calls anybody. Nobody waits for anybody.

So if events are the right primitive, what’s the right tool to carry them?

1.6 EDA vs Messaging vs Reactive: Clearing the Confusion

Three terms get used interchangeably in this space. They’re related, but they’re not the same thing. Getting this straight now will save confusion for the rest of the book.

Event-Driven Architecture (EDA) is an architectural style. Services communicate by producing and consuming events. No direct calls. No shared state. The events represent things that happened in your domain: OrderPlaced, PaymentFailed. EDA is the what and why of this book.

Message-Driven (or Message-Oriented) architecture is related but subtly different. In message-driven systems, services send commands, which are instructions for another service to do something. ProcessPayment. ReserveInventory. These are requests, not facts. The sender has intent; it wants something done. Messaging systems (like JMS or RabbitMQ) are often used this way.

The distinction matters because commands create coupling. If you send a ProcessPayment command, you’re implicitly depending on a payment processor being there to handle it. Events don’t carry that assumption. They just report what happened.

Reactive programming is a programming model: a way of writing code that deals with asynchronous data streams. Project Reactor, RxJava, Spring WebFlux are reactive programming frameworks. You can use reactive programming without EDA, and you can do EDA without reactive programming.

Think of it this way: - EDA tells you how your services relate to each other - Messaging tells you what kind of thing they’re sending each other - Reactive tells you how you write the code that handles it

Table 1.1: EDA vs messaging vs reactive programming.
What it is Example
EDA Architectural style Services communicate via events
Messaging Communication pattern Send commands to be processed
Reactive Programming model Non-blocking streams with Project Reactor

In this book, we’re doing EDA. We’ll use Kafka as the event backbone, Spring Boot as the application framework, and occasionally Spring’s reactive support where it makes sense. But the focus is always on the architectural patterns, not the reactive APIs.

With that cleared up, what does event-driven actually look like in code?

1.7 A Tale of Two Services

The best way to understand the difference between synchronous and event-driven is to see the same business logic written both ways.

The scenario: a customer places an order. We need to charge their payment method and reserve the inventory. Here’s how a synchronous implementation handles it, and here’s where it breaks.

1.7.1 The Synchronous Version

// OrderService - synchronous approach
public Order placeOrder(OrderRequest request) {
    try {
        inventoryClient.reserve(request.getItems()); // (1)
        paymentClient.charge(request.getPayment());  // (2)
        return orderRepository.save(new Order(request));
    } catch (PaymentException e) {
        inventoryClient.release(request.getItems()); // (3)
        throw e;
    }
}

(1) HTTP call to InventoryService: blocks until response

(2) HTTP call to PaymentService: blocks until response

(3) If payment fails, manually roll back the inventory reservation

This code works. Until InventoryService is slow. Or PaymentService is down. Or the rollback in (3) fails because InventoryService is now also down. Now you have reserved inventory, no payment, and no reliable way to clean up.

Warning

Manual rollback logic is fragile. If the rollback call fails, you now have two problems. This is exactly why the Saga pattern exists. We’ll cover it in Chapter 7.

The charge exists in the payment system. The order does not exist in your database. These are permanently inconsistent. There is no compensating transaction you can run without knowing the state of both systems. The only reliable recovery path is a human reviewing the payment ledger and manually reconciling the two records.

1.7.2 The Event-Driven Version

// OrderService - event-driven approach
public Order placeOrder(OrderRequest request) {
    Order order = orderRepository.save(
        new Order(request, OrderStatus.PENDING) // (1)
    );
    eventPublisher.publish(new OrderPlaced(order)); // (2)
    return order;
}

(1) Save the order immediately with PENDING status: no blocking calls

(2) Publish an OrderPlaced event: OrderService is done

// PaymentService - listens independently
@KafkaListener(topics = "order-placed")
public void on(OrderPlaced event) {
    var result = paymentService.charge(event.orderId()); // (3)
    eventPublisher.publish(new PaymentProcessed(
        result.paymentId(), event.orderId(), result.amount(), result.currency(), Instant.now()));
}

(3) PaymentService consumes the event on its own schedule, in its own process

Note

This uses @KafkaListener, a preview of what we’ll set up properly in Chapter 3. For now, focus on the shape of the code: PaymentService has no idea OrderService exists, and OrderService has no idea PaymentService exists.

The difference is stark. OrderService no longer waits for payment or inventory. It saves the order and publishes a fact: an order was placed. Everything else happens downstream, independently, in any order.

If PaymentService is down when the event is published, that’s fine. The event is stored in Kafka. PaymentService will process it when it recovers. OrderService never knew there was a problem.

This is what decoupling actually feels like: not just independent deployments, but independent failures.

Note

In this version, order placement is immediate but payment is eventual. The order starts as PENDING and transitions to CONFIRMED after payment succeeds. We’ll design this full state machine in Chapter 7 when we cover the Saga pattern.

The event-driven version is more complex to reason about at first. The payoff is resilience and scalability that the synchronous version simply cannot achieve.

So when should you make that trade?

1.8 When EDA Is the Right Choice (And When It Isn’t)

Event-driven architecture is not the right answer for every system. Applying it where it doesn’t belong adds complexity without benefit. Here’s how to think about the trade-off honestly.

1.8.1 Use EDA when:

Your system has high throughput requirements. Events decouple producers from consumers, which means each can scale independently. If your order volume spikes, OrderService publishes events at full speed without waiting for downstream services to keep up.

Loose coupling is genuinely important. If you have multiple independent teams owning separate services, or if you need to add new consumers without modifying producers, events are the right primitive. True independence, not just separate deployments.

Asynchronous processing is acceptable. In EDA, the response to an action (“your payment was processed”) is eventual, not immediate. If your business domain tolerates that, and most e-commerce workflows do, EDA fits well.

You need an audit trail. Events are immutable facts. Every OrderPlaced, PaymentProcessed, and InventoryReserved event is a record of what happened and when. This is invaluable for debugging, compliance, and the Event Sourcing pattern we’ll cover in Chapter 5.

1.8.2 Don’t use EDA when:

You’re building a simple CRUD application. A content management system, an admin dashboard, an internal tool. These don’t have the coupling or scale problems that EDA solves. Adding Kafka to a CRUD app is engineering for problems you don’t have.

Strong consistency is required. If your business rules demand that a write either fully succeeds or fully fails, with no intermediate states, EDA will fight you. Eventual consistency is a feature of event-driven systems, not a limitation you can engineer away.

The mechanism is simple: EDA delivers events asynchronously. By the time a consumer processes an event, a different producer may have already changed the underlying state. Getting two consumers to agree on the same ordering of writes requires coordination, and coordination reintroduces the coupling that EDA was designed to remove.

Your team is small and early. EDA adds operational overhead: a Kafka cluster to run, event schemas to version, distributed tracing to set up. A team of two building a new product should not be doing this on day one.

Your use case is predominantly read-heavy. If you’re serving queries more than processing commands, a well-designed read model with caching will outperform an event-driven pipeline for your primary use case.

1.8.3 The honest trade-off table:

Table 1.2: Synchronous REST vs event-driven trade-offs.
Synchronous REST Event-Driven
Simplicity ✅ Simple to understand ❌ Higher cognitive overhead
Consistency ✅ Strong consistency easy ❌ Eventual consistency only
Debugging ✅ Stack traces, easy to trace ❌ Distributed tracing required
Resilience ❌ Cascading failures ✅ Failure isolation
Scalability ❌ Coupled scaling ✅ Independent scaling
Team independence ❌ Shared coupling ✅ True independence
Throughput ❌ Bounded by slowest service ✅ Async, high throughput

EDA is not a free upgrade. It trades simplicity and strong consistency for resilience and scale. Make that trade consciously, not because it’s trendy.

So when you do decide to use it, what mistakes do teams make first?

1.9 Common Mistakes and Summary

  1. Adopting EDA before you have the problems it solves. EDA is a solution to scale and coupling. If your team is small and your traffic is manageable, the operational overhead isn’t worth it yet.

  2. Building a distributed monolith. Splitting services and keeping synchronous REST calls between them gives you the complexity of distribution without the benefits of decoupling. If services still call each other directly, you haven’t achieved independence.

  3. Treating events like commands. Publishing a ProcessPayment event is not event-driven architecture. It’s messaging with extra steps. Events are facts: PaymentProcessed. Commands are instructions: ProcessPayment. The distinction changes how you design your consumers.

  4. Ignoring eventual consistency in the UI. Event-driven systems are eventually consistent. If your frontend expects immediate confirmation of every action, you’ll have a mismatch between your architecture and your user experience. Design for it deliberately.

  5. Underestimating the operational overhead. Kafka needs monitoring, schema management, consumer lag alerting, and disaster recovery planning. This is real work. Factor it into your decision.


1.9.1 Summary

  • Monoliths are great early: one process, one deployment, strong consistency for free. They break down when the team, codebase, and traffic grow beyond a single process.

  • Synchronous microservices don’t solve coupling: they move it from compile-time to runtime. Cascading failures, latency chains, and distributed transaction problems are the price.

  • Events are immutable facts: past tense, fire-and-forget. Producers don’t know consumers. This is what real decoupling looks like.

  • EDA is a trade-off, not an upgrade: you gain resilience and independent scalability, but you lose strong consistency and simplicity. Make that trade deliberately.

  • Next: Chapter 2 goes inside Apache Kafka, the event backbone we’ll use throughout this book. We’ll cover topics, partitions, offsets, consumer groups, and how KRaft mode eliminates ZooKeeper entirely in Kafka 4.x.

Buy the full book on Leanpub Google Play Books