ZyVOP Logo
Content That Connects
SeriesAI NewsLeaderboardWrite for Us
ZyVOP Logo
Content That Connects

Empowering developers and creators with cutting-edge insights, comprehensive tutorials, and innovative solutions for the digital future.

Content

  • Categories
  • Tags
  • Badges
  • Leaderboard
  • Write Article
  • Newsletter

Company

  • About Us
  • API Documentation
  • Write for Us
  • Contact

Connect

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • DMCA Policy
  • Code of Conduct

Ā© 2026 ZyVOP. Crafted with care for the developer community.

Made with ā¤ļø by the ZyVOP team
All systems operational
HomeTutorialEvent-Driven Microservices with Apache Kafka, Redis Caching and Transactional Outbox Pattern
Tutorial

Event-Driven Microservices with Apache Kafka, Redis Caching and Transactional Outbox Pattern

A production-grade hands-on guide for Java & Spring Boot backend engineers.

Shubham Bhati
Shubham BhatiJava Backend Engineer | Microservices
August 16, 2026
3 min read
Series

Production-Grade Microservices Architecture with Java & Spring Boot

Part 3 of 3

Prev
Next
Event-Driven Microservices with Apache Kafka, Redis Caching and Transactional Outbox Pattern
#springboot#java#Kafka#Redis#Microservices#Architecture
šŸ‘1

When you scale microservices under high traffic, traditional HTTP REST calls between services break down quickly. Cascading timeouts, network latency spikes and database deadlocks will crash your order processing pipeline during traffic bursts.

I experienced this firsthand while building high-concurrency backend pipelines and optimizing infrastructure costs to under ₹100 per month (using Koyeb, Vercel and optimized DB pooling). That same low-cost, high-performance architecture enabled our e-commerce platform to generate ₹1 Lakh+ in revenue in just 2 months without single-point-of-failure downtimes.

Here is the exact production-grade blueprint for building event-driven Spring Boot microservices with Apache Kafka, Redis caching and the Transactional Outbox Pattern.


1. The Dual-Write Problem in Microservices

Suppose your order-service needs to save a new order to PostgreSQL and notify payment-service and inventory-service via Kafka.

If you write code like this:

@Transactional
public OrderResponse createOrder(CreateOrderRequest request) {
    Order order = orderRepository.save(new Order(request));
    kafkaTemplate.send("order-created-topic", new OrderCreatedEvent(order.getId()));
    return orderMapper.toResponse(order);
}

This code has a catastrophic flaw known as the Dual-Write Problem:

  1. If the database transaction commits but Kafka crashes right after, your order is saved but event consumers never receive it (data drift).

  2. If Kafka succeeds but the database transaction rolls back due to a constraint violation, an event is published for an order that never existed.


2. Solving Dual-Write with the Transactional Outbox Pattern

Instead of publishing to Kafka inside the database transaction, write an outbox record inside the same ACID transaction as your entity.

Outbox Entity

@Entity
@Table(name = "outbox_events")
public class OutboxEvent {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    @Column(nullable = false)
    private String aggregateType;

    @Column(nullable = false)
    private String aggregateId;

    @Column(nullable = false)
    private String eventType;

    @Column(columnDefinition = "TEXT", nullable = false)
    private String payload;

    @Column(nullable = false)
    private Instant createdAt;

    @Enumerated(EnumType.STRING)
    private OutboxStatus status = OutboxStatus.PENDING;

    // Constructors, getters and setters
}

Transactional Order Service Implementation

@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final OutboxRepository outboxRepository;
    private final ObjectMapper objectMapper;

    public OrderService(OrderRepository orderRepository, 
                        OutboxRepository outboxRepository, 
                        ObjectMapper objectMapper) {
        this.orderRepository = orderRepository;
        this.outboxRepository = outboxRepository;
        this.objectMapper = objectMapper;
    }

    @Transactional
    public OrderResponse createOrder(CreateOrderRequest request) {
        Order order = orderRepository.save(new Order(request.customerCode(), request.totalAmount()));

        OutboxEvent outbox = new OutboxEvent();
        outbox.setAggregateType("Order");
        outbox.setAggregateId(order.getId().toString());
        outbox.setEventType("ORDER_CREATED");
        outbox.setPayload(objectMapper.writeValueAsString(new OrderCreatedEvent(order.getId(), order.getTotalAmount())));
        outbox.setCreatedAt(Instant.now());

        outboxRepository.save(outbox);
        return new OrderResponse(order.getId(), "PENDING");
    }
}

3. High-Throughput Outbox Publisher with Spring Scheduler & Kafka

Now create a dedicated background publisher using Spring Virtual Threads (spring.threads.virtual.enabled=true) to process outbox records and publish to Apache Kafka:

@Component
public class OutboxPublisher {

    private static final Logger log = LoggerFactory.getLogger(OutboxPublisher.class);
    private final OutboxRepository outboxRepository;
    private final KafkaTemplate<String, String> kafkaTemplate;

    public OutboxPublisher(OutboxRepository outboxRepository, KafkaTemplate<String, String> kafkaTemplate) {
        this.outboxRepository = outboxRepository;
        this.kafkaTemplate = kafkaTemplate;
    }

    @Scheduled(fixedDelay = 500)
    @Transactional
    public void publishPendingEvents() {
        List<OutboxEvent> pending = outboxRepository.findTop50ByStatusOrderByCreatedAtAsc(OutboxStatus.PENDING);

        for (OutboxEvent event : pending) {
            kafkaTemplate.send("order-events", event.getAggregateId(), event.getPayload())
                .whenComplete((result, ex) -> {
                    if (ex == null) {
                        event.setStatus(OutboxStatus.PROCESSED);
                        outboxRepository.save(event);
                    } else {
                        log.error("Failed to publish outbox event id: {}", event.getId(), ex);
                    }
                });
        }
    }
}

4. Idempotent Consumer & Redis Distributed Locking

Network retries in Kafka mean your consumers WILL receive duplicate events. You must enforce idempotency at the consumer layer using Redis:

@Component
public class PaymentOrderConsumer {

    private static final Logger log = LoggerFactory.getLogger(PaymentOrderConsumer.class);
    private final StringRedisTemplate redisTemplate;

    public PaymentOrderConsumer(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @KafkaListener(topics = "order-events", groupId = "payment-service-group")
    public void consumeOrderEvent(ConsumerRecord<String, String> record) {
        String eventId = record.key();
        String lockKey = "idempotency:order:" + eventId;

        Boolean acquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "PROCESSED", Duration.ofHours(24));
        if (Boolean.FALSE.equals(acquired)) {
            log.info("Duplicate event detected for order id: {}. Skipping execution.", eventId);
            return;
        }

        // Process payment processing logic safely
        log.info("Successfully processed payment for order id: {}", eventId);
    }
}

Key Performance Results

By combining Virtual Threads, Redis Idempotency Keys and the Transactional Outbox Pattern:

  • Zero Data Loss: Guaranteed atomic persistence of business state and event logs.

  • Sub-10ms P99 Latency: REST responses return immediately after database write without waiting for external Kafka roundtrips.

  • ₹100/month Infra Cost: Lightweight memory footprint allowing full stack deployment on free/low-cost cloud tiers.

Shubham Bhati

Shubham Bhati

Java Backend Engineer | Microservices

Backend Engineer with 3+ years of experience building resilient microservices, event-driven pipelines and high-concurrency systems using Java, Spring Boot, Kafka, Redis and PostgreSQL. Engineered fintech transaction routing at MobilePe, B2B logistics pipelines at AlignBits and architected an e-commerce backend hosted under ₹100/month in infra costs that generated ₹1L+ revenue in 2 months. I write hands-on guides on Spring Cloud, service discovery, database indexing and low-cost architecture patterns.

Series

Production-Grade Microservices Architecture with Java & Spring Boot

Part 3 of 3

Prev
Next

Comments (0)

Login to post a comment.

Related Posts

Building a Reusable Keycloak Theme Architecture

Customizing a Keycloak login theme is styling. Doing it for a second brand without copying the first one is architecture. Here is how a base theme, four theme-resolution rules, and 58 design tokens turn a new branded login and email experience into twenty lines of properties and a logo.

Read article

The Command Palette Is an Architecture, Not a Widget

The theming article ended on a move from how an app looks to how power users drive it. The command palette — the Cmd+K menu that fuzzy-searches everything you c...

Read article

Code-Splitting Is a Boundary Decision, Not a Bundle Trick

The command palette article closed by promising the last piece of this look-and-feel stretch, and it's the one that looks the most like a solved problem: code-s...

Read article

You Probably Don't Need Multi-Agents

I work across six or seven repositories on one project — a big hybrid thing, part microfrontend, part backend, several apps that all talk to each other. When I ...

Read article

Centralized Secret Management with Spring Cloud Config & Vault: A Production Guide

Hardcoding DB credentials, JWT secrets or API keys inside application.yml or Docker images is a security disaster waiting to happen. Learn how to set up a production-grade Spring Cloud Config Server integrated with HashiCorp Vault for zero-downtime secret rotation across microservices.

Read article