· 5 min read ·
Building RelayMQ (Part 1): A Minimal Message Broker in Scala
Implementing the core broker functionalities: queues, messages, publish, consume, ack, and nack.
-
I recently started building RelayMQ, a message broker written in Scala, inspired by the internal architecture of message brokers like RabbitMQ.
-
The goal is to deeply understand how messaging systems actually work internally, by building my own version.
-
This post covers the first in-process, minimal but working version of RelayMQ.
What I Implemented
The first milestone was implementing the core broker functionalities.
At this stage, RelayMQ supports:
- Declaring queues
- Publishing messages
- Consuming messages
- Acknowledging messages
- Negative acknowledgements with requeue
The message lifecycle currently looks like this:
Producer
↓
Queue (READY)
↓
Consumer
↓
IN_FLIGHT
↓
ACK → removed
NACK → back to READYThis is the same at-least-once delivery pattern used by most message brokers.
Core Components
The first version of RelayMQ is a simple in memory message broker built using the following components:
- Broker
- Queues
- Network (Connections, Commands)
Broker
The broker orchestrates command execution and manages queue registries.
RelayMQBroker
|__ Map[String, RelayMQQueue]The RelayMQBroker also exposes an execute method that takes in a RelayMQCommand and returns a RelayMQResponse
def execute(command: RelayMQCommand): RelayMQResponsepackage relaymq.protocol
sealed trait RelayMQCommand
case class DeclareQueue(queue: String) extends RelayMQCommand
case class DeleteQueue(queue: String) extends RelayMQCommand
case class Publish(queue: String, payload: String) extends RelayMQCommand
case class Consume(queue: String) extends RelayMQCommand
case class Ack(queue: String, messageId: String) extends RelayMQCommand
case class NAck(queue: String, messageId: String) extends RelayMQCommandpackage relaymq.protocol
sealed trait RelayMQResponse
case class Ok(message: String) extends RelayMQResponse
case class MessageResponse(id: String, payload: Array[Byte]) extends RelayMQResponse
case class Error(message: String) extends RelayMQResponseEvery command coming from the network layer is executed through the broker.
Queue Registry
Each queue maintains two internal structures:
readyMessages
inFlightMessagesMessages move between these states:
READY → IN_FLIGHT → ACKED
READY → IN_FLIGHT → NACKED -> REQUEUEDThis allows the broker to support message redelivery when consumers fail to process the message.
Network Layer
A minimal TCP server that accepts client connections.
Commands are sent as simple text instructions.
The command module in the network layer parses commands to convert into a RelayMQCommand and forwards them to the broker.
DECLARE_QUEUE episodes
PUBLISH episodes intro
CONSUME episodes
ACK episodes <messageId>
NACK episodes <messageId>The command execution logic is fully managed by the Broker locally. Each queue also manage their own data locally, overseen by the Broker
Testing RelayMQ
Connecting to RelayMQ using telnet
telnet localhost 5672
Trying ::1...
Connected to localhost.
Escape character is '^]'.
DECLARE_QUEUE messagebox
Ok(Queue messagebox created)
PUBLISH messagebox HEY
Ok(Message produced successfully)
CONSUME messagebox
MessageResponse(ac7d2b75-005a-40f7-a308-4320dcd1df3c,[B@1fccccd7)
ACK messagebox ac7d2b75-005a-40f7-a308-4320dcd1df3c
Ok(Message ac7d2b75-005a-40f7-a308-4320dcd1df3c ack-ed successfully)Testing failures & negative acknowledgements:
PUBLISH messagebox Hello RelayMQ
Ok(Message produced successfully)
CONSUME messagebox
MessageResponse(4662a542-ce50-4d23-a8aa-0c4d38a71590,[B@ccb8a7d)
// No messages ready in the queue
CONSUME messagebox
Error(Error in consuming message from the Queue messagebox. Queue Size is 0)
// Ack-ing an incorrect message
ACK messagebox 4662a542
Error(Error in ack-ing message in the Queue messagebox)
// nAck-ing the message that was in-flight
NACK messagebox 4662a542-ce50-4d23-a8aa-0c4d38a71590
Ok(Message 4662a542-ce50-4d23-a8aa-0c4d38a71590 nAck-ed successfully)
// Consuming the nAck-ed message again
CONSUME messagebox
MessageResponse(4662a542-ce50-4d23-a8aa-0c4d38a71590,[B@ccb8a7d)
ACK messagebox 4662a542-ce50-4d23-a8aa-0c4d38a71590
Ok(Message 4662a542-ce50-4d23-a8aa-0c4d38a71590 ack-ed successfully)So the functionalities are working as expected, but the response formats need to be tuned, so they can be parsed deterministically
Why This?
Message brokers looked too simple from the outside. But once I started looking at implementing one from scratch, I felt the challenges that production grade message brokers would have faced/
- message state transitions
- consumer failures
- delivery guarantees
- concurrency
- reliability
Even this small prototype already exposed several interesting design questions.
For example,
- The current design uses a
synchronizedblock to protect the queues from concurrent operations. - But this won't scale well in a real system.
- As the number of producers and consumers grow, multiple threads will constantly compete for the same lock, which can quickly become a bottleneck.
- This locking behaviour significantly limits parallelism.
I'm currently analyzing the below for solving this case
- Single-writer principle
- Event-loop based processing
In both models, each queue processes operations sequentially within its own execution context. This removes the need for heavy locking while still maintaining correctness.
What Comes Next
My next plan for RelayMQ are:
- Consumer subscriptions
- Prefetch support
- Implementing Exchanges & Exchange-based routing
- Message persistence using a WAL (write-ahead log)
Stay Tuned!
Takeaway
- RelayMQ is still very simplistic, but even this minimal implementation already provides a clear view into how message brokers work & evolve internally.
Followup Topics
- Building RelayMQ (Part 2): Implenting Consumer Subscriptions
- Building RelayMQ (Part 3): Implenting Exchanges & Message Routing