DEV Community

Ankit Sood
Ankit Sood

Posted on

๐Ÿš€ How We Preserved Event Order in Kafka While Streaming From Cosmos DB: A Real-World Journey

Just publish the event after saving to the database. How hard can it be?

Famous last words. ๐Ÿ˜…

What started as a simple requirement in one of my projects quickly turned into a deep dive on ordering guarantees, idempotence, and the quirks of using Cosmos DBโ€™s Mongo API.

This is the story of how we used the Outbox Pattern, Spring Boot, and Apache Kafka to build a resilient system that preserved strict ordering of eventsโ€”even in the face of failures.

๐ŸŽฏ Goal

We were building an accounts payable backend on Azure:

๐Ÿ“ฆ Cosmos DB (Mongo API) for storing invoices.
๐Ÿ”ฅ Apache Kafka as the backbone for event-driven workflows.
๐ŸŒฑ Spring Boot for application logic.


๐ŸŽฏ Requirements

โœ… When a user places an order, save it to the database
โœ… Immediately publish an OrderConfirmed event to Kafka
โœ… Ensure downstream consumers always process events in order

Sounds simple enoughโ€ฆ right?


๐ŸŽฏ Approaches

๐Ÿ˜… The First Naive Attempt and Hereโ€™s how we started:

// 1. Save order
mongoTemplate.insert(order);

// 2. Publish to Kafka
kafkaTemplate.send("order-events", orderId, payload);
Enter fullscreen mode Exit fullscreen mode

This worked for some time But pretty soon, things blew up.

๐Ÿ”ฅ Problem #1: Lost Events
If Kafka was down after the DB write, the event was lost forever. Downstream services never found out about the order.

๐Ÿ”ฅ Problem #2: Out-of-Order Delivery
Even with retries, we saw scenarios like:

OrderShipped(order-123) arrives BEFORE OrderConfirmed(order-123)
Enter fullscreen mode Exit fullscreen mode

This broke downstream systems relying on proper sequencing.

๐Ÿงฉ Enter the Outbox Pattern to save the day

Instead of trying to send events directly to Kafka, we:

  • Extended our DB schema with an outbox collection
  • When saving an order, also saved a pending outbox event in the same transaction
  • Built a background publisher to read outbox events and send them to Kafka in order
  • Marked events as "SENT" only after Kafka acknowledged

Hereโ€™s the flow:

[App writes Order + Outbox event] โ†’ [Outbox Processor] โ†’ [Kafka]
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“ฆ Outbox pattern acts as a reliable buffer between your DB and Kafka. This pattern works well with any SQL Database but doesn't work with Cosmos DB mongo API.

โšก Challenges With Cosmos DB Mongo API

We knew Cosmos DBโ€™s Mongo API isnโ€™t MongoDB and Transactions only work if:
โœ… Both writes are in the same collection
โœ… Documents share the same partitionKey

So we designed a shared collection:

๐Ÿ—„ Structure of Documents

  • Order Document
{
  "_id": "order-123",
  "type": "order",
  "userId": "user-456",
  "status": "confirmed",
  "partitionKey": "user-456"
}
Enter fullscreen mode Exit fullscreen mode
  • Outbox Event Document
{
  "_id": "event-789",
  "type": "event",
  "eventType": "OrderConfirmed",
  "payload": { "orderId": "order-123" },
  "createdAt": "2025-06-27T12:00:00Z",
  "status": "PENDING",
  "partitionKey": "user-456"
}
Enter fullscreen mode Exit fullscreen mode

Both uses the same partitionKey to enable transactions.

๐Ÿ’ป The Implementation:

We have used Java and Spring boot to implement this. Below is how we put it all together:

๐Ÿ— Atomic Write Service
We ensured order + outbox event writes were atomic:

@Service
public class OrderService {
    @Autowired private MongoTemplate mongoTemplate;

    @Transactional
    public void createOrderWithEvent(Order order, OutboxEvent event) {
        mongoTemplate.insert(order, "sharedCollection");
        mongoTemplate.insert(event, "sharedCollection");
    }
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿš€ Kafka Outbox Publisher
A scheduled Spring component polls pending outbox events and sends them to Kafka in creation order.

@Component
public class OutboxEventPublisher {
    @Autowired private MongoTemplate mongoTemplate;
    @Autowired private KafkaTemplate<String, String> kafkaTemplate;

    @Scheduled(fixedDelay = 1000)
    public void publishEvents() {
        Query query = Query.query(Criteria.where("type").is("event").and("status").is("PENDING"))
                .with(Sort.by("createdAt")).limit(10);

        List<OutboxEvent> events = mongoTemplate.find(query, OutboxEvent.class, "sharedCollection");

        for (OutboxEvent event : events) {
            try {
                kafkaTemplate.send("order-events", event.getPartitionKey(), event.getPayload()).get();

                // Mark as SENT
                mongoTemplate.updateFirst(
                    Query.query(Criteria.where("_id").is(event.getId())),
                    Update.update("status", "SENT"),
                    "sharedCollection"
                );

                System.out.println("Published event: " + event.getId());

            } catch (Exception e) {
                System.err.println("Kafka send failed for event: " + event.getId());
                break; // Stop to avoid out-of-order sends
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿ” Kafka Producer Configuration
We enabled idempotence and sync sends to preserve order:

props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
props.put(ProducerConfig.ACKS_CONFIG, "all");
props.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 1);
Enter fullscreen mode Exit fullscreen mode

๐Ÿ— Transaction Manager Configuration
Springโ€™s @Transactional is not same as Transaction for Cosmos or Mongo.

  • It's designed for relational databases (JPA, JDBC, etc.)

  • It Relies on a transaction manager like PlatformTransactionManager

  • For MongoDB, it needs MongoDB 4.x+ with replica sets and Spring Data MongoDB transaction support.

@Bean
    MongoTransactionManager transactionManager(MongoDatabaseFactory dbFactory) {
        return new MongoTransactionManager(dbFactory);
    }
Enter fullscreen mode Exit fullscreen mode

To use MongoDB transactions via Spring Boot, all the below things must be true:
โœ… MongoDB server version โ‰ฅ 4.0
โœ… Connected to a replica set (Cosmos DB Mongo API v4.0+ does support transactions but only under certain conditions)
โœ… Use MongoTransactionManager explicitly in Spring

But Cosmos DB Mongo API:
โš  Only supports transactions within the same partition (documents must share partitionKey)
โš  Has subtle differences from native MongoDB transactions

In case, a transaction should be started explicitly, we can use

try (ClientSession session = mongoTemplate.getMongoDbFactory().getMongoClient().startSession()) {
    session.startTransaction();

    try {
        mongoTemplate.withSession(() -> session).insert(order, "sharedCollection");
        mongoTemplate.withSession(() -> session).insert(event, "sharedCollection");
        session.commitTransaction();
    } catch (Exception e) {
        session.abortTransaction();
        throw e;
    }
}
Enter fullscreen mode Exit fullscreen mode

๐ŸŽฏ Final Thoughts

The Outbox Pattern helped us bridge the gap between Cosmos DB and Kafka. Itโ€™s a robust, production-ready solution for ordered, reliable event streamingโ€”even when your DB doesnโ€™t support native CDC.

CDC was one more approach that we could have used. In the past, we have used cosmos change stream which had its own issues and hence we decided to go ahead with this.

๐Ÿ‘‰ Have you faced ordering challenges in Kafka pipelines? How did you solve them? Share your experiences below!

References

Top comments (0)