Spring BootJavaMicroservicesKafkaEDA

Spring Boot Microservices & Event-Driven Architecture in 2026

A deep-dive into building production-grade, event-driven microservices with Spring Boot 3.x in 2026 — covering Kafka, Saga patterns, observability, and container deployment.

·12 min read·Hamdullah Hamdard

The State of Microservices in 2026

By 2026, microservices have moved from hype to mainstream engineering practice — and the tooling has caught up. Spring Boot 3.x (running on Java 21 with virtual threads via Project Loom) erases the performance gap with reactive stacks while keeping the familiar imperative programming model. Pair that with Kafka for asynchronous communication, Kubernetes for orchestration, and OpenTelemetry for observability, and you have a stack capable of handling enterprise-grade workloads with a comparatively small team.

Why Event-Driven Architecture (EDA)?

In a synchronous microservice mesh, every service call adds latency and creates tight coupling — if the downstream service is slow or down, the upstream request fails. Event-Driven Architecture decouples services through a message broker: producers publish domain events to a topic; consumers process those events independently and at their own pace. This delivers three critical properties at scale:

  • Loose coupling — producers have no knowledge of their consumers; adding a new consumer requires zero changes to the producer.
  • Resilience — the broker durably stores events; a consumer can go offline and catch up when it recovers.
  • Scalability — consumers scale horizontally by adding more instances to a consumer group, with Kafka distributing partitions automatically.

Project Structure: Domain-Driven Microservices

Organise each service around a bounded context from Domain-Driven Design (DDD). A typical e-commerce platform might have Order, Inventory, Payment, and Notification services — each owning its own database and communicating exclusively through events.

text
// Recommended Maven module layout for an Order service
order-service/
├── src/main/java/com/hamdard/order/
│   ├── domain/          // Entities, Value Objects, Domain Events
│   │   ├── Order.java
│   │   └── events/
│   │       └── OrderPlacedEvent.java
│   ├── application/     // Use-cases / Command Handlers
│   │   └── OrderCommandService.java
│   ├── infrastructure/  // Kafka producers, JPA repos, REST controllers
│   │   ├── kafka/
│   │   │   └── OrderEventPublisher.java
│   │   └── persistence/
│   │       └── OrderJpaRepository.java
│   └── api/             // REST controllers, DTOs
│       └── OrderController.java
└── src/main/resources/
    └── application.yml

Publishing Domain Events with Spring Kafka

Spring for Apache Kafka wraps the native client in a clean, Spring-idiomatic API. The KafkaTemplate handles serialization, partitioning, and acknowledgment. Always send events after the database transaction commits — Spring's @TransactionalEventListener with AFTER_COMMIT phase guarantees this.

java
// domain/events/OrderPlacedEvent.java
public record OrderPlacedEvent(
    String orderId,
    String customerId,
    List<OrderItem> items,
    BigDecimal totalAmount,
    Instant occurredAt
) {}

// infrastructure/kafka/OrderEventPublisher.java
@Component
@RequiredArgsConstructor
public class OrderEventPublisher {

    private final KafkaTemplate<String, OrderPlacedEvent> kafkaTemplate;

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onOrderPlaced(OrderPlacedEvent event) {
        kafkaTemplate.send("orders.placed", event.orderId(), event)
            .whenComplete((result, ex) -> {
                if (ex != null) {
                    log.error("Failed to publish OrderPlacedEvent: {}", ex.getMessage());
                } else {
                    log.info("OrderPlacedEvent published to partition {}",
                        result.getRecordMetadata().partition());
                }
            });
    }
}

Consuming Events and the Saga Pattern

When a business transaction spans multiple services (e.g. place order → reserve inventory → charge payment), you need a way to coordinate and compensate for failures. The Saga pattern handles this via a sequence of local transactions, each publishing an event that triggers the next step. If any step fails, compensating events roll back the previous steps.

java
// Inventory service consumes OrderPlacedEvent and either reserves stock
// or publishes InventoryReservationFailed to trigger a compensating action.

@Component
@RequiredArgsConstructor
public class InventoryEventConsumer {

    private final InventoryService inventoryService;
    private final KafkaTemplate<String, Object> kafkaTemplate;

    @KafkaListener(topics = "orders.placed", groupId = "inventory-service")
    public void handleOrderPlaced(OrderPlacedEvent event) {
        try {
            inventoryService.reserve(event.orderId(), event.items());
            kafkaTemplate.send("inventory.reserved",
                new InventoryReservedEvent(event.orderId()));
        } catch (InsufficientStockException ex) {
            kafkaTemplate.send("inventory.reservation.failed",
                new InventoryReservationFailedEvent(event.orderId(), ex.getMessage()));
        }
    }
}

// application.yml — consumer configuration
spring:
  kafka:
    consumer:
      group-id: inventory-service
      auto-offset-reset: earliest
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
      properties:
        spring.json.trusted.packages: "com.hamdard.*"

Idempotency and Exactly-Once Semantics

In distributed systems, the network can fail after the broker has accepted a message but before the producer receives the acknowledgment — meaning the producer may retry and create a duplicate. Consumers can also receive the same message more than once (at-least-once delivery). You must design consumers to be idempotent.

  • Store a processed event ID in the database alongside the business mutation in the same transaction.
  • Before processing, check whether the event ID already exists — skip if it does.
  • Kafka's `enable.idempotence=true` and transactional producers give exactly-once delivery on the broker side.
  • Use Spring Kafka's `SeekToCurrentErrorHandler` with a dead-letter topic (DLT) for poison messages that repeatedly fail.
java
@KafkaListener(topics = "orders.placed", groupId = "payment-service")
@Transactional
public void handleOrderPlaced(OrderPlacedEvent event) {
    // Idempotency check
    if (processedEventRepository.existsByEventId(event.orderId())) {
        log.warn("Duplicate event ignored: {}", event.orderId());
        return;
    }

    paymentService.charge(event);
    processedEventRepository.save(new ProcessedEvent(event.orderId(), Instant.now()));
}

Observability: Traces, Metrics, and Logs

A distributed system that you cannot observe is a system you cannot operate. In 2026 the standard stack is OpenTelemetry (instrumentation) → Grafana Tempo (traces) → Prometheus (metrics) → Grafana (dashboards). Spring Boot 3.x auto-configures Micrometer Tracing with OpenTelemetry out of the box — you just add the dependency.

xml
<!-- pom.xml — auto-configured distributed tracing -->
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>

# application.yml
management:
  tracing:
    sampling:
      probability: 1.0   # 100% in dev; reduce to 0.1 in prod
  otlp:
    tracing:
      endpoint: http://tempo:4318/v1/traces
  metrics:
    export:
      prometheus:
        enabled: true

logging:
  pattern:
    level: "%5p [%X{traceId:-},%X{spanId:-}]"  # Inject trace IDs into every log line

Container Deployment with Docker and Kubernetes

Each microservice ships as a container image. Spring Boot's `./mvnw spring-boot:build-image` command creates an OCI-compliant image using Cloud Native Buildpacks — no Dockerfile needed. In production, deploy to Kubernetes with a Deployment, HorizontalPodAutoscaler, and PodDisruptionBudget for each service.

yaml
# Build the image (no Dockerfile required)
./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=order-service:1.0.0

# kubernetes/order-service-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-service
  template:
    spec:
      containers:
        - name: order-service
          image: order-service:1.0.0
          ports:
            - containerPort: 8080
          env:
            - name: SPRING_KAFKA_BOOTSTRAP_SERVERS
              valueFrom:
                secretKeyRef:
                  name: kafka-credentials
                  key: bootstrap-servers
          resources:
            requests:
              cpu: "250m"
              memory: "512Mi"
            limits:
              cpu: "1000m"
              memory: "1Gi"
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080

Key Takeaways

Building event-driven microservices with Spring Boot in 2026 is more approachable than ever, but it still requires deliberate design decisions. To summarise the most important ones:

  • Design around bounded contexts and domain events, not around database tables.
  • Use Kafka's consumer groups to scale consumers horizontally without code changes.
  • Implement the Saga pattern for distributed transactions — avoid distributed two-phase commit.
  • Make every consumer idempotent: the broker guarantees at-least-once, not exactly-once delivery.
  • Instrument with OpenTelemetry from day one — retrofitting observability is painful.
  • Ship each service as a container and let Kubernetes handle scaling, restarts, and zero-downtime deploys.

Hamdullah Hamdard

Full Stack Mobile Developer · Flutter · Laravel · Spring Boot · Next.js

Get in touch