NATS is a messaging system that can be used to build microservices, but it’s not a traditional RPC or REST API. Instead, you build your microservices as NATS services, which communicate with each other by sending and receiving messages over NATS. This approach offers several advantages, including loose coupling, scalability, and resilience.

Let’s see how this works in practice.

A Simple NATS Service

Imagine we have a simple "greeter" service that responds to requests with a greeting.

First, we need to install NATS:

go install github.com/nats-io/nats-server/v2/nats-server@latest

Then, start the NATS server:

nats-server

Now, let’s create our greeter service. This service will listen for messages on the subject greeter.greet. When it receives a message (which we’ll assume is a name), it will publish a greeting back on the reply subject.

Here’s the Go code for the greeter service:

package main

import (
	"log"
	"time"

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

func main() {
	// Connect to NATS
	nc, err := nats.Connect(nats.DefaultURL)
	if err != nil {
		log.Fatalf("Failed to connect to NATS: %v", err)
	}
	defer nc.Close()
	log.Println("Connected to NATS")

	// Subscribe to the "greeter.greet" subject
	sub, err := nc.Subscribe("greeter.greet", func(msg *nats.Msg) {
		name := string(msg.Data)
		greeting := "Hello, " + name + "!"
		log.Printf("Received request for %s, sending: %s", name, greeting)
		// Publish the greeting back to the reply subject
		err := nc.Publish(msg.Reply, []byte(greeting))
		if err != nil {
			log.Printf("Error publishing reply: %v", err)
		}
	})
	if err != nil {
		log.Fatalf("Failed to subscribe to greeter.greet: %v", err)
	}
	defer sub.Unsubscribe()

	log.Println("Greeter service started. Listening on greeter.greet")

	// Keep the service running
	select {}
}

To run this, save it as greeter.go and compile/run:

go run greeter.go

You should see output like:

[12345] 2023/10/27 10:00:00 Connected to NATS
[12345] 2023/10/27 10:00:00 Greeter service started. Listening on greeter.greet

Now, let’s create a client that will send a request to this service. The client will publish a message to greeter.greet and wait for a reply on a unique, automatically generated reply subject.

Here’s the Go code for the client:

package main

import (
	"log"
	"time"

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

func main() {
	// Connect to NATS
	nc, err := nats.Connect(nats.DefaultURL)
	if err != nil {
		log.Fatalf("Failed to connect to NATS: %v", err)
	}
	defer nc.Close()
	log.Println("Connected to NATS")

	name := "World"
	subject := "greeter.greet"

	// Publish a request and wait for a reply
	msg, err := nc.Request(subject, []byte(name), 5*time.Second)
	if err != nil {
		log.Fatalf("Request to %s failed: %v", subject, err)
	}

	log.Printf("Received reply from %s: %s", subject, string(msg.Data))
}

Run this client:

go run client.go

You’ll see the client output:

[67890] 2023/10/27 10:01:00 Connected to NATS
[67890] 2023/10/27 10:01:00 Received reply from greeter.greet: Hello, World!

And the greeter service will show:

[12345] 2023/10/27 10:00:30 Received request for World, sending: Hello, World!

This demonstrates a basic request-reply pattern using NATS. The nc.Request function in the client automatically creates a temporary queue subscription for the reply and sends the message to the greeter.greet subject. The greeter service receives the message, processes it, and publishes the response to the msg.Reply subject, which the client is listening on.

The Mental Model: Subjects as Endpoints

In NATS, communication is organized around subjects. Subjects are simply strings, like greeter.greet or orders.new. Think of them as named channels or topics. Services publish messages to subjects, and other services subscribe to subjects they are interested in.

The power of NATS services comes from how these subjects are managed and how NATS routes messages.

  • Publish/Subscribe: A service can publish a message to a subject, and any number of subscribers to that subject will receive it. This is great for event-driven architectures (e.g., orders.created event published to all interested services).
  • Request/Reply: As seen above, a client can send a request to a subject and expect a single reply. NATS handles the routing of the reply back to the correct requester. This is akin to RPC or REST.
  • Queue Groups: Multiple instances of the same service can subscribe to a subject using a queue group. NATS ensures that only one member of the queue group receives any given message. This is crucial for load balancing and fault tolerance. If you have three greeter services in the greeters queue group, and a message arrives on greeter.greet, only one of those three services will process it. If one service fails, NATS will automatically start sending its messages to the remaining healthy instances.

Scaling and Resilience

To scale the greeter service, you simply run more instances of the greeter.go program, all subscribing to greeter.greet with the same queue group name (e.g., qgroup="greeters").

// Inside the greeter.go main function, when subscribing:
sub, err := nc.QueueSubscribe("greeter.greet", "greeters", func(msg *nats.Msg) {
    // ... service logic ...
})
// ...

Now, if you run three greeter.go instances with this change, NATS will distribute incoming greeter.greet requests among them. If one instance crashes, the other two will pick up the slack.

Configuration and Discovery

NATS doesn’t have a built-in service registry like Consul or etcd. Service discovery is implicit: clients know the subject names they need to communicate with. This is a deliberate design choice to keep NATS simple and fast. For more complex discovery needs, you can build a service registry on top of NATS itself or use other tools.

For example, a "service discovery" service could subscribe to service.register messages and maintain a list of active services. Other services could then query this discovery service for available endpoints.

The "One Thing" That Makes it Different

The most surprising thing about NATS services is how it leverages the inherent simplicity of its subject-based messaging to achieve sophisticated patterns like request-reply and queue groups without complex coordination protocols. The server’s job is primarily efficient message routing. The "intelligence" for service behavior and resilience lies in how clients and services subscribe to subjects and form queue groups, with NATS acting as the intelligent, high-performance fabric. It’s like having a hyper-efficient postal service where you just drop letters into mailboxes (subjects) and the postal workers (NATS server) know exactly where to deliver them, even if there are multiple recipients for the same mailbox (queue groups) or if you need a reply slip attached (request-reply).

This fundamental approach allows for incredibly flexible and robust microservice architectures. The next step is often exploring more advanced NATS features like JetStream for durable message queuing and stream processing.

Want structured learning?

Take the full Nats course →