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.createdevent 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
greetersqueue group, and a message arrives ongreeter.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.