The Actor Model: The Secret Weapon for High-Concurrency Message Processing

Introduction

In the previous two articles, we covered JuggleIM's overall architecture and long-connection design. Today, let's explore one of JuggleIM's most distinctive technology choices — the Actor model.

If you've ever written multi-threaded programs, you're certainly familiar with concepts like locks, deadlocks, and race conditions. Under high concurrency, these problems get magnified to infinity and become a nightmare for system stability.

JuggleIM avoids these problems at the root by introducing the Actor model, while keeping the code simple and maintainable.

Let's see how this concurrency model, originally from Erlang, is implemented in Go and supports an IM system with hundreds of millions of daily active users.

1. The Predicament of Traditional Multithreading

Before discussing the Actor model, let's review the challenges of traditional multithreaded programming.

The Nightmare of Shared State

In traditional multithreaded programming, multiple threads share the same piece of memory. When multiple threads read and write the same variable at the same time, a race condition can occur.

For example, a simple counter with two threads both executing "read — add 1 — write back" may end up with 1 instead of the expected 2.

To solve this, we need to introduce locks. But locks bring new problems:

Performance degradation: acquiring and releasing locks has overhead, and threads block frequently under heavy lock contention.

Deadlock risk: multiple threads waiting on each other's locks, leaving all threads stuck.

Code complexity: lock granularity and acquisition order need to be carefully designed; one slip and things break.

Special Challenges of IM

IM system concurrency is even more complex:

  • Tens of thousands of users online simultaneously: each user can send messages at any time
  • Real-time message processing required: latency-sensitive
  • Many states: user state, conversation state, group member list...

Using traditional lock mechanisms for these scenarios makes the code extremely complex and bugs hard to find.

2. The Core Idea of the Actor Model

The Actor model offers a completely different concurrency approach. Its core idea can be summarized in three statements:

Idea 1: Everything Is an Actor

In the Actor model, Actor is the basic unit of computation. You can think of an Actor as an independent little person, with their own private space (state), doing things their own way (processing logic).

Each Actor:

  • Has its own private state, not shared with other Actors
  • Has its own mailbox for receiving messages
  • Processes messages from the mailbox in order

Idea 2: Message-Driven Communication

Actors cannot directly access each other's state — they can only communicate by sending messages.

When Actor A wants Actor B to do something, A doesn't directly call B's method; instead:

  1. A sends a message to B
  2. The message enters B's mailbox
  3. B retrieves the message from the mailbox at the right time and processes it

It's like sending email rather than making a phone call — asynchronous and decoupled.

Idea 3: Serial Message Processing

Each Actor processes only one message at a time. Only when the current message is finished does the next one get processed.

This means that within an Actor, no locks are needed. There's no situation where multiple threads execute the Actor's code simultaneously.

3. JuggleIM's Actor Implementation

Now that we understand the theory, let's see how JuggleIM applies it in practice.

Core Interface Design

JuggleIM defines a core interface IUntypedActor. All business handlers implement this interface:

IUntypedActor {
    OnReceive(ctx, msg)  // Handle the received message
}

Very simple — only the OnReceive method needs to be implemented, telling the framework what to do when a message arrives.

Actor Registration Mechanism

When the service starts, each module registers its own Actors:

  • The Message service registers PrivateMsgActor to handle private-chat messages
  • The Group service registers GroupMsgActor to handle group-chat messages
  • The HistoryMsg service registers QryHistoryMsgsActor to handle history message queries
  • ...

Each business logic is encapsulated in an independent Actor, with single responsibility and clean code.

Message Routing

When a request arrives, the framework finds the corresponding Actor based on the message type and dispatches the message to that Actor's mailbox.

For example, when a private-chat message arrives:

  1. The connection layer parses the message and identifies it as a private-chat message
  2. The framework finds the PrivateMsgActor registered for "private-chat message" handling
  3. An Actor instance is created (or an existing one is reused)
  4. The message is dispatched to that Actor

Concurrent Processing

JuggleIM's Actor framework supports two modes:

Normal mode: A new Actor instance is created for each incoming message, leveraging Go goroutines' lightweight nature for high concurrency.

Standalone mode: A fixed number of Actor instances, with messages distributed through a queue. Suitable for scenarios requiring controlled concurrency, such as the message-dispatch core path.

For example, the message-dispatch Actor is configured with 6,144 concurrent instances, ensuring processing capacity while avoiding resource exhaustion.

4. Core Advantages of the Actor Model

Advantage 1: Inherent Thread Safety

An Actor processes only one message at a time, and all state operations are serial. No locks are needed, so no deadlocks.

For developers, this means focusing on business logic without worrying about concurrency control — greatly reducing cognitive load and the chance of bugs.

Advantage 2: Highly Decoupled

Actors communicate via messages, not direct method calls. This brings several benefits:

  • Location transparency: the caller doesn't care which thread/process/machine the Actor runs on
  • Failure isolation: one Actor's crash doesn't affect other Actors
  • Easy to test: just simulate sending messages and verify the result

Advantage 3: Naturally Supports Distribution

The message-driven model extends easily to distributed scenarios:

  • Within a single machine: messages pass through in-memory queues
  • Across machines: messages pass through the network

JuggleIM's architecture is already ready for clustering.

Advantage 4: Natural Backpressure

Each Actor has its own mailbox (queue). When upstream messages arrive faster than the Actor can process, they accumulate in the mailbox, forming a natural buffer.

This is more elegant than traditional direct calls — a slow link won't block the entire call chain.

5. Practical Analysis of Business Actors

Let's look at a few concrete business Actors to see how they're applied in practice.

Private-Chat Message Actor

PrivateMsgActor is responsible for handling private-chat messages and is one of the core Actors.

When it receives a private-chat message, its processing logic is:

Step 1: Persist the message

  • Call the HistoryMsg service to store the message
  • Generate the message ID and timestamp

Step 2: Update the conversation

  • Update the sender's conversation list
  • Update the recipient's conversation list
  • Increment the unread count

Step 3: Distribute the message

  • Check whether the recipient is online
  • If online, push it to the long connection
  • If offline, trigger push notifications

Step 4: Return ACK

  • Return a send-success confirmation to the sender

The whole process is completed serially within a single Actor. The logic is clear, with no concurrency concerns.

Group Message Actor

GroupMsgActor handles group messages, which is more complex than private chat:

Step 1: Permission validation

  • Check that the sender is a group member
  • Check whether the group is muted
  • Check whether the sender is muted

Step 2: Persist the message

  • Store it in the group history message table
  • Associate the group snapshot (record the group member list at send time)

Step 3: Batch distribution

  • Get the group member list
  • Generate an inbox record for each member
  • Batch push to online members

The complexity of group messages comes from the fact that there can be hundreds or thousands of recipients. But thanks to the Actor model, the core logic is still a clean serial flow.

Message Sync Actor

SyncMsgActor handles the client's message-sync request:

Step 1: Parse the sync point

  • The client reports its last local sync timestamp

Step 2: Query new messages

  • Query the inbox from the timestamp onward
  • Also query the outbox (the sender's own messages)

Step 3: Assemble the response

  • Sort messages by time
  • Return them to the client
  • Indicate whether more messages are available

6. The Perfect Marriage of Actor and Go Goroutines

JuggleIM's choice of Go to implement the Actor model is a brilliant combination.

Goroutine Characteristics

Go's goroutines are very lightweight:

  • Initial stack of only 2KB — easy to spawn hundreds of thousands
  • Low scheduling overhead, fast context switching
  • The GMP scheduler efficiently utilizes multiple cores

This means we can create a goroutine for each Actor request, without the need to maintain a thread pool like in Java.

Channel as Mailbox

Go's channels are a natural fit for an Actor's mailbox:

  • Support multiple producers
  • Blocking reads
  • Built-in synchronization

An Actor reads messages from the channel, processes them, and continues to the next — perfectly matching the Actor's serial-processing model.

Concise Code Style

Go's interface mechanism makes Actor implementation very simple. Each business Actor only needs to:

  1. Define a struct
  2. Implement the OnReceive method
  3. Register it with the framework

No inheritance, no complex configuration — out of the box.

7. Performance Considerations

Although the Actor model is elegant, there are performance issues to watch.

Message Serialization Overhead

Actors communicate via messages, which need to be serialized / deserialized. JuggleIM uses Protobuf, whose binary serialization is highly efficient, and this overhead is acceptable.

Avoiding Mailbox Buildup

If the Actor's processing speed can't keep up with the message arrival rate, the mailbox grows, and may eventually cause out-of-memory errors.

JuggleIM's countermeasures:

  • Monitor mailbox length
  • Core Actors use Standalone mode to control concurrency
  • The connection layer applies rate limiting to control request rate at the source

Avoiding Internal Blocking

Actors should not perform long-blocking operations while processing a message. If you must call an external service or database:

  • Use timeout-bounded calls
  • Consider going async, with callbacks to handle the result

8. From Actor to a Larger Philosophy of Concurrency

The Actor model's lessons go beyond the IM scenario:

Lesson 1: Sharing is the root of concurrency problems

Instead of working hard to protect shared resources, design them out. Each Actor has its own state and exposes only a message interface.

Lesson 2: Asynchrony is an effective decoupling tool

Synchronous calls create strong coupling. Message-driven asynchronous communication lets parts of the system evolve independently.

Lesson 3: Simple rules produce complex behavior

The Actor model has very simple rules: private state, message communication, serial processing. But a system composed of countless Actors can accomplish extraordinarily complex tasks.

These ideas are not only applicable to IM, but to the design of any high-concurrency distributed system.

image

Project URL: https://github.com/juggleim/im-server