What I Love About Apache Kafka

Taming the Beast: Concurrency Control in Distributed Architectures

Building distributed systems is hard—no two ways about it. Today's microservice architectures often advocate for smaller, stand-alone applications, eschewing statefulness and session affinity. However, in a large system where any instance must be capable of handling any request from any client—and must frequently consult other distributed components to do so—meeting requirements for things like mutual exclusion and data integrity becomes a monumental challenge. This isn't up for debate; it's Computer Science 101.

Confronted with this reality, architects typically consider at least these four strategies:

  1. Design to Circumvent Requirements: Avoid the requirements from the get-go.

  2. The Holy Grail of Eventual Consistency: Invoke eventual consistency as a magical solution to a host of problems.

  3. Solve Problems at the Application Layer: Attempt to resolve concurrency issues within the application itself.

  4. Message Bus/Event Stream to the Rescue: Introduce a central message bus/event stream platform as a mediator.

Strategies 1 and 2 have their place, but they aren't universally applicable. Eventual consistency, in particular, is a frequently misunderstood and misapplied concept. It’s a viable solution only for a subset of issues.

Strategy 3 smells like trouble. These issues fall far outside your business domain, and they are exceptionally difficult to resolve. If you do manage to solve them, you might just have inadvertently recreated Kafka or built your own distributed transaction manager. This path is perilous and should be avoided.

Strategy 4 is deceptive. Your average message bus/event stream platform, especially a distributed one, doesn't inherently solve these problems. These platforms often prioritize scalability over coordination, which generally means they lack robust message ordering guarantees.

Enter Kafka: The Best of Both Worlds

Here's where Apache Kafka shines like a beacon. Kafka offers strong guarantees that can fulfill most of your concurrency requirements, an accomplishment that seems almost paradoxical. How? The answer lies in Kafka's secret weapon: Partitions.

Embracing Intelligent Stickiness

Unlike other systems that compromise coordination for scalability, Kafka ingeniously uses partitions to achieve both. Within a single partition, there is only one active Kafka Broker responsible for the event stream, and only one consumer group member processing events for that partition at any given time.

This design is Kafka’s way of reintroducing 'stickiness' into the microservice world, but in a strategic and controlled manner. It ensures that messages related to a specific key are directed, or ‘stuck’, to one exact partition, maintaining a focused, coherent processing thread amidst the sprawling chaos of distributed architecture.

In-Process Concurrency Control

This structure doesn’t just streamline processing—it provides a powerful tool for developers. By confining events of a certain key to a specific partition, Kafka allows developers to handle concurrency requirements in-process, significantly simplifying what can be an extraordinarily complex part of distributed systems programming. In essence, it repositions the responsibility for concurrency from the infrastructure into a single micro service instance, where developers have full control.

Or, in other words: the behind-the-scene, state-of-the-art consensus algorithm that Kafka relies on to gracefully handle network issues, node failures, and other distributed system challenges, allows developers to work within one of the most straightforward coding contexts imaginable—The Single-Threaded Environment!

Mastering Concurrency with Thoughtful Partition Key Selection

In Kafka, the selection of the right partition key for each topic is a critical decision. It’s not just about load balancing or message ordering; it’s fundamentally about managing concurrency. By aligning the partition key with the natural boundaries of your business domain, you can isolate concurrent operations in a way that makes logical sense and avoids conflicts.

Consider a collaborative document editing application, where multiple users can edit different documents, or even different sections of the same document, at the same time. In this scenario, a poorly chosen partition key, like using a fixed key for all messages or the user ID, can lead to unnecessary serialization of unrelated edits, causing sluggish performance and poor user experience.

A wisely chosen partition key, such as the document_id + section_id, can elegantly resolve this concurrency dilemma. Here's how:

  1. When using document_id as a part of the partition key, all edits to a specific document are routed to the same partition. This ensures that updates to the same document are processed in order, eliminating the risk of conflicts at the document level.

  2. By further refining the partition key with section_id, edits to different sections of the same document can be processed concurrently in different partitions, allowing for high parallelism and responsiveness, while still maintaining integrity within each section.

In this example, choosing the right partition key not only maintains the integrity of each document and its sections but also allows for maximum parallel processing of edits across different sections and documents. The concurrency challenge, which could be a severe headache in a naive distributed system, becomes a well-organized and efficient process under Kafka’s management, thanks to the thoughtful selection of a partition key.

The Irony of Total Distribution

While the microservices trend is steering towards the promise of total distribution, it often inadvertently creates a maze of complexity. Here's the irony: the same architects who praise the virtues of total distribution are often the same people who turn to Kafka to solve their most vexing problems. And why? Because Kafka, in its wisdom, opts for locality over distribution, knowing that this decision is a safeguard against overwhelming complexity.

The Kafka Difference

In a nutshell, Kafka’s partitions represent a significant stride forward, allowing developers to sidestep some of the most treacherous pitfalls associated with distributed systems. With Kafka, developers can focus more on solving business problems and less on wrestling with the inherent complexities of distributed systems.

It's a kind of magic—that rare blend of brilliant engineering and insightful design that makes Kafka not just another tool in the box, but a game-changer in the world of distributed systems. And that, in essence, is what I love about Kafka.

Back to blog