DEV Community

Cover image for Diving into Actor Model with Go and NATS
Barani Kumar S
Barani Kumar S

Posted on

Diving into Actor Model with Go and NATS

Introduction

Working with Golang is always fun and satisfying. But when it comes to scaling your application on a distributed computing infrastructure, you start to feel the real pain. To reduce such pain, numerous models and patterns were discovered. Some are cloud native (Kubernetes for example), while others need to be implemented at the architectural level. One such model is called Actor Model. We'll dive deep into what Actor Model is in this blog.

What is Actor Model?

Actor Model Arch Created by GPT5
The actor model is a model of concurrent computation where independent entities, called actors, communicate asynchronously via messages.

An actor is an independent unit that encapsulates its own state and behavior. Actors do not share state directly; instead, they interact only through message passing.

One major advantage of Actor Model is it's fault tolerant nature. Since each actor is isolated and interacts only through asynchronous messages, an anomaly or failure in one actor will not directly affects other actors. Other actors are designed in a way that it can tackle the situations where it doesn't receive message from faulty actor.

You can also easily diagnose the source of error and you need to replace only the faulty actor while rest of the systems operate normally.

For example, consider a chat application. Each user can be represented as an actor with its own state (e.g., username, online status, chat history). When one user (actor) sends a message to another, the receiving actor updates its own state accordingly—in this case, by adding the new message to its chat history.

Actor Model vs Microservice Model

The below table explains the difference between Actor and Microservice Model in context of distributed computation,

Feature Actor Model Microservice Model
State Encapsulated per actor Often externalized (DB per service)
Communication Async messages only Async or sync (HTTP, gRPC, messaging)
Coupling Very loose (message-only contracts) Looser than monolith, but DB/API coupling still common
Granularity Fine-grained (per actor) Coarse-grained (per service/app)
Scaling Each actor independently Each service independently
Failure isolation Actor-level Service-level

Now that we understand how the Actor Model works conceptually, let’s see how we can implement it practically using NATS in Go.

NATS in Actor Model

NATS Logo
At the core, NATS is a cloud-native messaging system written in go, which enables applications communicate via Publish/Subscribe pattern. Now you will get the idea on where we are going :). NATS also supports other messaging patterns like Request/Reply and Streaming support with it's inbuilt JetStream functionality. We're going to use NATS's Pub/Sub functionality to transfer messages between actors in our Actor Model.

Simple actor in Golang with NATS

Alright, enough lectures, we're now going to dive straight into writing code. For this we create a basic chat application where 2 users (User A and User B) sends message back and forth,

Firstly, make sure you downloaded NATS Server and run it,

$ ./nats-server
Enter fullscreen mode Exit fullscreen mode

Now, create a Go module and install github.com/nats-io/nats.go package,

$ go get github.com/nats-io/nats.go
Enter fullscreen mode Exit fullscreen mode

In the main.go file, establish a connection to NATS. Here the nats.DefaultURL is nats://localhost:4222,

package main

import (
    "log"

    "github.com/nats-io/nats.go"
)

func main() {
    nc, err := nats.Connect(nats.DefaultURL)
    if err != nil {
        log.Fatal(err)
    }
    defer nc.Drain()
}
Enter fullscreen mode Exit fullscreen mode

We're going to make a terminal chat application, so we first need to ask user's name. So, update the code accordingly

package main

import (
    "bufio"
    "fmt"
    "log"
    "os"
    "strings"

    "github.com/nats-io/nats.go"
)

func main() {
    nc, err := nats.Connect(nats.DefaultURL)
    if err != nil {
        log.Fatal(err)
    }
    defer nc.Drain()

    // Ask for username
    reader := bufio.NewReader(os.Stdin)
    fmt.Print("Enter your name: ")
    name, _ := reader.ReadString('\n')
    name = strings.TrimSpace(name)
}
Enter fullscreen mode Exit fullscreen mode

Next, we define the NATS subject name. A subject is basically a route which is used by NATS to route the messages. Think of it like a route in HTTP Url. Our chat application will listen to this subject to publish and subscribe messages.

We also perform nats.Subscribe which takes in subject name and a callback function. Whenever a message gets published to the subject, this callback function is invoked and you can perform actions inside the callback function,

func main() {
    nc, err := nats.Connect(nats.DefaultURL)
    if err != nil {
        log.Fatal(err)
    }
    defer nc.Drain()

    // Ask for username
    reader := bufio.NewReader(os.Stdin)
    fmt.Print("Enter your name: ")
    name, _ := reader.ReadString('\n')
    name = strings.TrimSpace(name)

    subject := "chat.room1"

    // Subscribe to chat messages
    _, err = nc.Subscribe(subject, func(m *nats.Msg) {
        fmt.Printf("\n%s\n> ", string(m.Data)) // reprint prompt after message
    })
    if err != nil {
        log.Fatal(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, here is the final part. We create a loop which runs indefinitely, where we print subscribed messages and publish our own messages,

func main() {
    // Connect to NATS
    nc, err := nats.Connect(nats.DefaultURL)
    if err != nil {
        log.Fatal(err)
    }
    defer nc.Drain()

    // Ask for username
    reader := bufio.NewReader(os.Stdin)
    fmt.Print("Enter your name: ")
    name, _ := reader.ReadString('\n')
    name = strings.TrimSpace(name)

    subject := "chat.room1"

    // Subscribe to chat messages
    _, err = nc.Subscribe(subject, func(m *nats.Msg) {
        fmt.Printf("\n%s\n> ", string(m.Data)) // reprint prompt after message
    })
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("Connected to chat! Type messages and press Enter. Type '/quit' to exit.")
    fmt.Print("> ")

    // Read user input loop
    for {
        text, _ := reader.ReadString('\n')
        text = strings.TrimSpace(text)

        if text == "/quit" {
            fmt.Println("Exiting chat...")
            return
        }

        if text != "" {
            msg := fmt.Sprintf("[%s @ %s]: %s", name, time.Now().Format("15:04:05"), text)
            nc.Publish(subject, []byte(msg))
        }
        fmt.Print("> ")
    }
}
Enter fullscreen mode Exit fullscreen mode

And that's it, we created our own chat application that leverages NATS. Now, we'll test it by running 2 instances of our app (we need 2 users to chat 😆).

Open 2 terminals and run the application. Below is a video that demonstrate actor Model in action,

You can see that both applications maintains it's own state (username in our case) and communicates via a single subject (chat.room1).

And that's it for this blog. See you in another one 😉.

Top comments (0)