Careeroza
Kafka Core Concepts
Topics, Partitions, Offsets
| Concept | Meaning |
|---|---|
| Broker | A Kafka server. A cluster has several brokers. |
| Topic | A named stream of events, e.g. order-placed. |
| Partition | A topic is split into partitions (an append-only log each). More partitions = more parallel consumers. |
| Offset | Position of a record inside a partition: 0, 1, 2... Records are kept after reading (retention, e.g. 7 days). |
| Key | Records with the same key go to the same partition, so they stay in order (e.g. key = userId or orderId). |
| Consumer group | Consumers sharing a group.id split the partitions between them. Each group gets every record. |
| Committed offset | Where a group has read up to, per partition. A restarted consumer continues from there. |
| Replication | Each partition is copied to N brokers so a broker crash loses nothing. |
An In-memory Topic
This plain-Java model of a topic shows keys choosing partitions, offsets, two consumer groups each receiving every event, and partitions split between the consumers of one group:
Example
import java.util.*;
public class Main {
public static void main(String[] args) {
Topic topic = new Topic("order-placed", 3);
String[][] orders = {{"user-42", "1001", "2998"}, {"user-7", "1002", "1499"}, {"user-42", "1003", "799"},
{"user-13", "1004", "54999"}, {"user-7", "1005", "349"}, {"user-42", "1006", "1299"}};
for (String[] o : orders) {
OrderPlaced e = new OrderPlaced(Long.parseLong(o[1]), o[0], Integer.parseInt(o[2]));
int[] pos = topic.send(o[0], e);
System.out.println("producer: " + e + " -> partition " + pos[0] + ", offset " + pos[1]);
}
ConsumerGroup payments = new ConsumerGroup("payment-service", topic, List.of("payment-A", "payment-B"));
ConsumerGroup notifications = new ConsumerGroup("notification-service", topic, List.of("notify-A"));
payments.poll();
notifications.poll();
System.out.println("--- payment-service polls again: nothing new ---");
payments.poll();
topic.send("user-7", new OrderPlaced(1007, "user-7", 4999));
System.out.println("--- one new event ---");
payments.poll();
}
}
record OrderPlaced(long orderId, String userId, int amount) {}
record Message(String key, OrderPlaced value) {}
class Topic {
final List<List<Message>> partitions = new ArrayList<>();
Topic(String name, int count) {
for (int i = 0; i < count; i++) partitions.add(new ArrayList<>());
}
int[] send(String key, OrderPlaced value) {
int p = Math.floorMod(key.hashCode(), partitions.size()); // same key -> same partition
partitions.get(p).add(new Message(key, value));
return new int[] {p, partitions.get(p).size() - 1};
}
}
class ConsumerGroup {
private final String groupId;
private final Topic topic;
private final Map<String, List<Integer>> assignment = new LinkedHashMap<>();
private final int[] committed;
ConsumerGroup(String groupId, Topic topic, List<String> consumers) {
this.groupId = groupId;
this.topic = topic;
this.committed = new int[topic.partitions.size()];
consumers.forEach(c -> assignment.put(c, new ArrayList<>()));
for (int p = 0; p < committed.length; p++) assignment.get(consumers.get(p % consumers.size())).add(p);
System.out.println(groupId + " assignment: " + assignment);
}
void poll() {
assignment.forEach((consumer, parts) -> {
for (int p : parts) {
List<Message> log = topic.partitions.get(p);
while (committed[p] < log.size()) {
System.out.println(" [" + groupId + "/" + consumer + "] p" + p + "@" + committed[p] + " " + log.get(committed[p]).value());
committed[p]++; // commit the offset
}
}
});
}
}Output
producer: OrderPlaced[orderId=1001, userId=user-42, amount=2998] -> partition 2, offset 0
producer: OrderPlaced[orderId=1002, userId=user-7, amount=1499] -> partition 0, offset 0
producer: OrderPlaced[orderId=1003, userId=user-42, amount=799] -> partition 2, offset 1
producer: OrderPlaced[orderId=1004, userId=user-13, amount=54999] -> partition 0, offset 1
producer: OrderPlaced[orderId=1005, userId=user-7, amount=349] -> partition 0, offset 2
producer: OrderPlaced[orderId=1006, userId=user-42, amount=1299] -> partition 2, offset 2
payment-service assignment: {payment-A=[0, 2], payment-B=[1]}
notification-service assignment: {notify-A=[0, 1, 2]}
[payment-service/payment-A] p0@0 OrderPlaced[orderId=1002, userId=user-7, amount=1499]
[payment-service/payment-A] p0@1 OrderPlaced[orderId=1004, userId=user-13, amount=54999]
[payment-service/payment-A] p0@2 OrderPlaced[orderId=1005, userId=user-7, amount=349]
[payment-service/payment-A] p2@0 OrderPlaced[orderId=1001, userId=user-42, amount=2998]
[payment-service/payment-A] p2@1 OrderPlaced[orderId=1003, userId=user-42, amount=799]
[payment-service/payment-A] p2@2 OrderPlaced[orderId=1006, userId=user-42, amount=1299]
[notification-service/notify-A] p0@0 OrderPlaced[orderId=1002, userId=user-7, amount=1499]
[notification-service/notify-A] p0@1 OrderPlaced[orderId=1004, userId=user-13, amount=54999]
[notification-service/notify-A] p0@2 OrderPlaced[orderId=1005, userId=user-7, amount=349]
[notification-service/notify-A] p2@0 OrderPlaced[orderId=1001, userId=user-42, amount=2998]
[notification-service/notify-A] p2@1 OrderPlaced[orderId=1003, userId=user-42, amount=799]
[notification-service/notify-A] p2@2 OrderPlaced[orderId=1006, userId=user-42, amount=1299]
--- payment-service polls again: nothing new ---
--- one new event ---
[payment-service/payment-A] p0@3 OrderPlaced[orderId=1007, userId=user-7, amount=4999]Note: Real Kafka uses the murmur2 hash of the key bytes, not String.hashCode(), but the rule is the same: same key → same partition → strict order for that key. There is no order across partitions.
Run Kafka Locally
Example
Kafka 3.x runs without ZooKeeper (KRaft mode). One container is enough for learning:
# docker-compose.yml
services:
kafka:
image: apache/kafka:3.8.0
container_name: kafka
ports:
- "9092:9092"
docker compose up -d
docker exec kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 \
--create --topic order-placed --partitions 3 --replication-factor 1
docker exec kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 \
--describe --topic order-placedExample output
Created topic order-placed. Topic: order-placed TopicId: q1Xb7mR2T0uY8kZp3nW5aA PartitionCount: 3 ReplicationFactor: 1 Configs: Topic: order-placed Partition: 0 Leader: 1 Replicas: 1 Isr: 1 Topic: order-placed Partition: 1 Leader: 1 Replicas: 1 Isr: 1 Topic: order-placed Partition: 2 Leader: 1 Replicas: 1 Isr: 1