DEV Community

Kim Seon Woo
Kim Seon Woo

Posted on

Building a Distributed Chat App without Redis/Kafka

Full source code: examples-seonwkim/spring-boot-chat

Actor library: seonWKim/spring-boot-starter-actor


πŸ€” Why Actors?

Most distributed chat systems rely on middleware like Redis Pub/Sub or Kafka to deliver messages between users on different servers. But there's an alternative: clustering servers with a distributed actor system.

In this post, I’ll walk you through building a fully functional distributed chat app using:

  • 🧩 Pekko (Akka’s Apache fork)
  • βš™οΈ Spring Boot 3
  • πŸ”Œ WebSocket + WebFlux
  • πŸ’¬ Actors for session & room handling

πŸ“¦ Setup: spring-boot-starter-actor

To reduce integration complexity, I created spring-boot-starter-actor, a library that brings native actor system support into Spring Boot.

Key dependencies:

dependencyManagement {
    imports {
        // pekko-serialization-jackson_3 require minimum 2.17.3 version of jackson
        mavenBom("com.fasterxml.jackson:jackson-bom:2.17.3")
    }
}

dependencies {
    implementation("io.github.seonwkim:spring-boot-starter-actor_3:0.0.25")
    implementation("org.springframework.boot:spring-boot-starter-webflux")
    implementation("org.springframework.boot:spring-boot-starter-websocket")
    implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
}
Enter fullscreen mode Exit fullscreen mode

🧠 Architecture Overview

Image description

The application is structured around two core actor types:

UserActor ↔ ChatRoomActor
Enter fullscreen mode Exit fullscreen mode
  • UserActor – Handles one user's session.
  • ChatRoomActor – Represents a unique room across the cluster.
  • WebSocketHandler – Bridges WebSocket and actor system.

Each user is represented by an actor that joins/leaves rooms and sends/receives messages. The room actor manages broadcasting to all users.


πŸ’¬ WebSocket Setup

Create a WebSocket endpoint via Spring config:

@Configuration
@EnableWebSocket
class WebSocketConfig(
    private val chatWebSocketHandler: ChatWebSocketHandler
) : WebSocketConfigurer {
    override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) {
        registry.addHandler(chatWebSocketHandler, "/ws/chat").setAllowedOrigins("*")
    }
}
Enter fullscreen mode Exit fullscreen mode

This sets up /ws/chat as your chat entrypoint.


πŸ§‘β€πŸ’» UserActor (One per connected user)

Each connected WebSocket user is handled by their own actor:

class UserActor : SpringActor {
    data class JoinRoom(val roomId: String) : Command
    data class SendMessage(val message: String) : Command
    ...
}
Enter fullscreen mode Exit fullscreen mode

Behavior:

  • On connect β†’ Sends connected event
  • On JoinRoom β†’ Registers with the ChatRoomActor
  • On message β†’ Relays to ChatRoomActor
  • On cluster event β†’ Receives JoinRoomEvent, LeaveRoomEvent, SendMessageEvent

🏠 ChatRoomActor (Cluster-sharded)

Rooms are unique across the cluster thanks to Pekko's Cluster Sharding:

class ChatRoomActor : ShardedActor<ChatRoomActor.Command> {
    data class JoinRoom(val userId: String, val userRef: ActorRef<UserActor.Command>) : Command
    data class SendMessage(val userId: String, val message: String) : Command
    ...
}
Enter fullscreen mode Exit fullscreen mode

Each room:

  • Tracks active users
  • Broadcasts join/leave/message events
  • Lives on a node chosen by Pekko’s sharding strategy

🌐 WebSocket Handler

Bridges incoming WebSocket traffic to actors:

override fun handleTextMessage(session: WebSocketSession, message: TextMessage) {
    when (payload.get("type").asText()) {
        "join" -> userActor.tell(JoinRoom(roomId))
        "message" -> userActor.tell(SendMessage(message))
    }
}
Enter fullscreen mode Exit fullscreen mode

Also handles connection setup/teardown and assigns userIds.


πŸš€ Running Multiple Nodes (Clustering)

Spin up a 3-node cluster with one script:

sh cluster-start.sh 8080 2551 3
Enter fullscreen mode Exit fullscreen mode

Each instance gets a unique port and Pekko cluster port. Open localhost:8080, 8081, and 8082 in different tabsβ€”voilΓ , distributed chat!

Image description


βœ… Why This Rocks

  • ❌ No Redis/Kafka/3rd-party message queue
  • βœ… Horizontal scalability with Pekko sharding
  • 🧠 Stateful logic using just actors
  • πŸ•Έ Seamless WebSocket session per user
  • 🎯 Built with familiar Spring Boot ergonomics

πŸ›  Try it Yourself


🧭 Next Steps

  • Make the actor library production-grade (v1.0.0)
  • Add observability and failover strategies
  • Integrate persistent actors for message durability

πŸ’¬ Feedback, questions, contributions β€” all welcome!

Let me know if you'd like a follow-up post diving into actor design patterns or cluster internals.

Top comments (0)