Published on

Distributed messaging with NATS

6 min read

Authors
banner

Recently, I was building an application where I wanted to take event-driven approach for async communication between the microservices. Usually, I would use Apache Kafka, RabbitMQ, Redis Streams or a managed solution like AWS SNS, Google Cloud Pub/Sub but this time I wanted to keep my implementation easy and cost minimal yet not affect my scalability or increase technical debt for the future. After a few hops on StackShare, I found NATS as a popular alternative and after trying it out, I really liked it. That evening I migrated everything from Apache Kafka to NATS. So, in this article, we'll learn what NATS is and how to get started with it.

What is NATS?

nats

NATS is an open-source messaging system. It provides a simple, secure, and performant communications system for digital systems, services, and devices. The core design principles of NATS are performance, scalability, and ease of use. Its server can run on-premise, in the cloud, at the edge, and even on a Raspberry Pi. NATS can secure and simplify the design and operation of modern distributed systems. It is written in Go and used by companies like Tesla, Paypal, Walmart, and many others. NATS is also part of the Cloud Native Computing Foundation (CNCF).

Note: For more info checkout NATS.io or watch this fantastic Keynote by its creator Derek Collison.

Features

Here are some of the features that I found interesting for my use case:

  • Ease of use
  • Highly performant
  • Zero downtime scaling
  • Self healing and resilient
  • Isolated and secure by default
  • Supports edge, cloud or hybrid deployments.

Getting started

Before we start with any code, let's see what we will implement. We will try to make a simple pub/sub example like shown in the diagram below, and we will use different languages and pretend they are different services. demo

Setup NATS server

Before anything, we'll need to set up a NATS server. We can do it multiple ways as shown below. Personally, I like to use docker but feel free to set it up however you want. We can also use demo.nats.io which is a demo server provider by NATS authors (please don't use it for production!).

Docker

Here's our docker-compose.yml file:

version: '3.8'

services:
  nats:
    container_name: nats
    image: nats:2.7.0-alpine
    ports:
      - 4222:4222
$ docker compose up

Or we can simply use docker run as well.

$ docker run -p 4222:4222 nats:2.7.0-alpine

Locally

As I'm using macOS, I installed it with brew.

$ brew install nats-server
$ nats-server

Note: Checkout official docs for more installation options.

Output

[18193] 2022/01/20 16:00:05.581377 [INF] Starting nats-server
[18193] 2022/01/20 16:00:05.581992 [INF]   Version:  2.7.0
[18193] 2022/01/20 16:00:05.581996 [INF]   Git:      [not set]
[18193] 2022/01/20 16:00:05.582005 [INF]   Name:     ND2VU7MH7J6RU6RS6JUKKPPCTMRPY35LRBRFT3NLENDZI3TL33PVRR3P
[18193] 2022/01/20 16:00:05.582008 [INF]   ID:       ND2VU7MH7J6RU6RS6JUKKPPCTMRPY35LRBRFT3NLENDZI3TL33PVRR3P
[18193] 2022/01/20 16:00:05.582791 [INF] Listening for client connections on 0.0.0.0:4222
[18193] 2022/01/20 16:00:05.583066 [INF] Server is ready

Client

Now that our NATS server is running, we'll be using Go and Node.js clients to connect to it for a simple demonstration. Not familiar with Go or Node? Don't worry NATS has clients available in over 40 languages!

First, let's init our Go module.

$ go mod init example
$ go get github.com/nats-io/nats.go/@latest
$ touch main.go

Our Go code will act as the subscriber.

package main

import (
	"fmt"

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

var subject = "my_subject"

func main() {
	wait := make(chan bool)

	nc, err := nats.Connect(nats.DefaultURL)

	if err != nil {
		fmt.Println(err)
	}

	nc.Subscribe(subject, func(m *nats.Msg) {
		fmt.Printf("Received: %s\n", string(m.Data))
		nc.Publish(m.Reply, []byte("Hello"))
	})

	fmt.Println("Subscribed to", subject)

	<-wait
}

Now, let's init our Node project.

$ npm init -y
$ npm install nats
$ touch index.js

Here is our JavaScript code that will act as the publisher.

const { connect, StringCodec } = require('nats');

const subject = 'my_subject';
const servers = 'localhost:4222';

async function demo() {
  const codec = StringCodec();
  const nc = await connect({ servers });

  nc.publish(subject, codec.encode('Hello there!'));

  console.log('Sent...');

  await nc.drain();
}

demo();

We will subscribe to the my_subject subject by running our Go code first.

$ go run main.go
Subscribed to my_subject

Now, We will run our JavaScript code, which publishes Hello there! message on my_subject subject.

$ node index.js
Sent...

And there it is! We can see the message being received by our subscriber. NATS makes this so simple, yet so powerful!

Subscribed to my_subject
Received: Hello there!

Conclusion

We haven't even scratched the surface in this article, NATS also has a built-in distributed persistence system called JetStream which takes it to a whole another level!

Lastly, I think NATS is a fantastic technology, it scales well from a hobby project to production ready distributed applications. And best of all it's quite easy to get started. I hope this article was helpful in getting you interested!

© 2024 Karan Pratap Singh