An educational, from-scratch simulation of the core ideas behind Apache Kafka, built with Spring Boot (backend) and React + Vite + Tailwind (frontend).
No Apache Kafka dependency is used anywhere in this project. Topics,
partitions, offsets, producers, consumers and consumer groups are all
implemented using plain Java collections, ExecutorService, and
thread-safe data structures.
Mini Kafka simulates four independent "microservices" — OrderService,
PaymentService, NotificationService, and UserService — that never call
each other directly. Instead, they publish and subscribe to topics through
a broker that behaves like a simplified Kafka. A React dashboard visualizes
everything happening inside the broker in real time over WebSocket.
Apache Kafka is a distributed event-streaming platform. Producers publish records to named topics; each topic is split into partitions for parallelism and ordering; consumers read records from partitions, tracking their position with an offset; and consumer groups let multiple consumers coordinate reading a topic. Kafka is durable, replicated, and horizontally scalable across a cluster of brokers.
Mini Kafka reproduces the same concepts — topics, partitions, offsets, producers, consumers, consumer groups — but as a single in-process, in-memory Java application. There's no replication, no disk persistence, and no cluster; it's meant purely to make the concepts tangible and observable through a live dashboard.
OrderService
|
v
Producer
|
v
MINI KAFKA (BrokerService)
|
+--+----------------+
| |
v v
Payment Consumer Notification Consumer
| |
v v
PaymentService NotificationService
OrderServicepublishes to theorderstopic.PaymentServiceandNotificationServiceeach independently consumeorders— neither knows the other exists.PaymentServicepublishes topayments;NotificationServicealso consumespayments.UserServicepublishes tousers.
All communication happens only through BrokerService.
A producer is anything that calls ProducerService.publish(topic, content),
which delegates to BrokerService.publishMessage(). This:
- Auto-creates the topic if it doesn't exist (matching real Kafka's default).
- Picks a partition via round-robin.
- Appends the message and assigns it the next offset.
- Broadcasts the new message and updated stats over WebSocket.
BrokerService is the single source of truth. It owns every Topic,
every ConsumerGroup, and every Consumer, using ConcurrentHashMap
for thread safety since multiple producer/consumer threads (including the
simulation's ScheduledExecutorService tasks) hit it concurrently.
A Topic is a named stream backed by one or more Partitions. Partition
selection for new messages is round-robin.
A Partition is an ordered, append-only log (CopyOnWriteArrayList) with
a monotonically increasing offset counter (AtomicLong). Once written, a
message is never modified.
Each Consumer tracks its own offset per partition in a
ConcurrentHashMap<Integer, Long>. This is what allows two different
consumers — say, payment-service-consumer and
notification-service-orders-consumer — to both read the orders topic
from the beginning without stepping on each other's progress.
A ConsumerGroup is a named collection of consumers (e.g.
payment-group, notification-group). Groups exist to organize
consumers logically; in this simplified model, offset tracking still
lives on the individual Consumer, which is enough to demonstrate that
groups have independent progress from one another.
An offset is the position of a message within a partition, starting at 0 and incrementing by 1 for every message appended to that partition. Consumers advance their own offset as they read; producers never touch offsets directly.
The backend uses STOMP over WebSocket (spring-boot-starter-websocket).
Whenever BrokerService.publishMessage() runs, it broadcasts:
- The new message to
/topic/messages - Updated dashboard stats to
/topic/stats
The frontend connects via SockJS + @stomp/stompjs (see
src/hooks/useWebSocket.js) and updates the UI without polling for the
live stream (a 5s polling fallback still refreshes topic/consumer lists
for consistency).
Requirements: Java 17+ and Maven 3.8+.
cd backend
mvn clean installRequirements: Node.js 18+.
cd frontend
npm installTerminal 1 — backend (starts on port 8080):
cd backend
mvn spring-boot:runTerminal 2 — frontend (starts on port 5173, proxies /api and /ws
to the backend — see vite.config.js):
cd frontend
npm run devOpen http://localhost:5173 in your browser. Click ▶ Start Simulation on the dashboard to see live traffic, or use the "Publish Message" form to send messages manually.
Note: this project was generated in a sandboxed environment without internet access, so the Maven and npm dependency downloads above have not been executed or verified end-to-end. Please run
mvn clean installandnpm installyourself and fix any dependency-version issues that surface (e.g. bumpingspring-boot-starter-parentor npm package versions) before relying on this in production.
| Method | Path | Description |
|---|---|---|
| POST | /api/topics |
Create a topic. Body: { "name": "orders", "partitions": 3 } |
| GET | /api/topics |
List all topics with summary stats |
| GET | /api/topics/{topicName} |
Get topic detail including all partitions |
| DELETE | /api/topics/{topicName} |
Delete a topic |
| GET | /api/topics/{topicName}/messages |
All messages in a topic, across partitions |
| Method | Path | Description |
|---|---|---|
| POST | /api/messages |
Publish a message. Body: { "topic": "orders", "message": "Order #101 placed" } |
| Method | Path | Description |
|---|---|---|
| POST | /api/consumers/subscribe |
Subscribe. Body: { "consumerId": "...", "groupId": "...", "topic": "orders" } |
| GET | /api/consumers/{consumerId}/messages |
Fetch new messages for a consumer (advances its offset) |
| GET | /api/consumers |
List all consumers |
| GET | /api/consumers/{consumerId} |
Get one consumer's summary |
| Method | Path | Description |
|---|---|---|
| GET | /api/dashboard/stats |
Live counts: topics, messages, producers, consumers, msgs/sec |
| Method | Path | Description |
|---|---|---|
| POST | /api/simulation/start |
Start the demo services |
| POST | /api/simulation/stop |
Stop the demo services |
| GET | /api/simulation/status |
{ "running": true/false } |
| Destination | Description |
|---|---|
/ws (SockJS) |
Connect endpoint |
/topic/messages |
New message broadcasts |
/topic/stats |
Updated dashboard stats |
Add screenshots of the Dashboard, Topics, and Consumers pages here once you've run the app locally.
- Database persistence (the backend is already layered so a repository
implementation could swap in behind
BrokerServicewithout touching controllers) - Authentication / authorization
- Multiple brokers + replication
- Message retention / compaction policies
- Consumer groups with multiple consumers splitting partitions
- Docker / Kubernetes deployment
- Metrics and observability (e.g. Micrometer + Prometheus)
mini-kafka/
├── backend/
│ ├── pom.xml
│ └── src/main/
│ ├── java/com/minikafka/
│ │ ├── MiniKafkaApplication.java
│ │ ├── controller/
│ │ │ ├── TopicController.java
│ │ │ ├── ProducerController.java
│ │ │ ├── ConsumerController.java
│ │ │ ├── DashboardController.java
│ │ │ └── SimulationController.java
│ │ ├── service/
│ │ │ ├── BrokerService.java
│ │ │ ├── TopicService.java
│ │ │ ├── ProducerService.java
│ │ │ ├── ConsumerService.java
│ │ │ └── SimulationService.java
│ │ ├── model/
│ │ │ ├── Message.java
│ │ │ ├── Topic.java
│ │ │ ├── Partition.java
│ │ │ ├── Consumer.java
│ │ │ └── ConsumerGroup.java
│ │ ├── dto/
│ │ │ ├── PublishMessageRequest.java
│ │ │ ├── CreateTopicRequest.java
│ │ │ ├── SubscribeRequest.java
│ │ │ └── MessageResponse.java
│ │ ├── websocket/
│ │ │ ├── WebSocketConfig.java
│ │ │ └── MessageBroadcaster.java
│ │ └── exception/
│ │ ├── GlobalExceptionHandler.java
│ │ ├── TopicNotFoundException.java
│ │ ├── TopicAlreadyExistsException.java
│ │ ├── InvalidMessageException.java
│ │ └── ConsumerNotFoundException.java
│ └── resources/application.properties
└── frontend/
├── package.json, vite.config.js, tailwind.config.js, postcss.config.js
├── index.html
└── src/
├── main.jsx, App.jsx, index.css
├── api/client.js
├── hooks/useWebSocket.js
├── components/
│ ├── Sidebar.jsx, Header.jsx, StatsCards.jsx
│ ├── TopicList.jsx, MessageStream.jsx, PublishMessage.jsx
│ ├── ConsumerPanel.jsx, PartitionView.jsx
└── pages/
├── Dashboard.jsx, Topics.jsx, Consumers.jsx
- User fills in the Publish Message form and clicks Publish (or
the simulation's
OrderServicefires on its own schedule). - Frontend calls
POST /api/messages. ProducerController→ProducerService→BrokerService.publishMessage().- The broker auto-creates the topic if needed, picks a partition via round-robin, and appends the message with the next sequential offset.
- The broker broadcasts the new message over
/topic/messagesand fresh stats over/topic/stats. - Every connected dashboard receives both broadcasts instantly via STOMP/WebSocket and updates the Live Message Stream and Overview cards with no page refresh.
- Independently,
PaymentServiceandNotificationService(running as scheduled tasks) poll their own subscriptions viaConsumerService.consume(), which fetches only the messages after each consumer's own last-read offset per partition, then advances that offset — completely decoupled from any other consumer's progress.