Communicating Between Microservices Using REST API in Go
When building microservices, it’s common for services to communicate with each other.
In this tutorial, we’ll walk through setting up communication between two microservices: a User Service and an Order Service.
The Order Service will verify user information by calling the User Service.
Prerequisites
- Basic knowledge of Go.
- Two Go Microservices : User Service and Order Service.
- HTTP server set up in both services.
Step-by-Step Guide
Setting Up the User Service
First, let’s create the User Service. This service will manage user data and expose an endpoint to verify user IDs.
Initialize the User Service
Create a new directory for the User Service:
mkdir user-service
cd user-service
go mod init user-service
Create the User Struct and Mock Data
Create a main.go file:
package main
import (
"encoding/json"
"fmt"
"net/http"
)
type User struct {
ID string `json:"id"`
Name string `json:"name"`
}
var users = []User{
{ID: "1", Name: "Alice"},
{ID: "2", Name: "Bob"},
}
func main() {
http.HandleFunc("/verify", verifyUserHandler)
fmt.Println("User Service Up and Running on port 8081")
err := http.ListenAndServe(":8081", nil)
if err != nil {
panic(err)
}
}
func verifyUserHandler(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
for _, user := range users {
if user.ID == id {
w.WriteHeader(http.StatusOK)
err := json.NewEncoder(w).Encode(user)
if err != nil {
fmt.Println("Error: ", err.Error())
}
return
}
}
w.WriteHeader(http.StatusNotFound)
err := json.NewEncoder(w).Encode(map[string]string{"error": "User not found"})
if err != nil {
panic(err)
}
}
Run the User Service
go run main.go
Your User Service is now running on localhost:8081 and can verify users by their IDs.
Setting Up the Order Service
Now, let’s create the Order Service, which will communicate with the User Service to verify user IDs.
Initialize the Order Service
Create a new directory for the Order Service:
mkdir order-service
cd order-service
go mod init order-service
Create the Order Struct and Order Endpoint
Create a main.go file:
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
)
type Order struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Item string `json:"item"`
}
func main() {
http.HandleFunc("/create", createOrderHandler)
fmt.Println("User Service Up and Running on port 8082")
err := http.ListenAndServe(":8082", nil)
if err != nil {
panic(err)
}
}
func createOrderHandler(w http.ResponseWriter, r *http.Request) {
var order Order
err := json.NewDecoder(r.Body).Decode(&order)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Verify the user ID
if !verifyUserID(order.UserID) {
http.Error(w, "Invalid user ID", http.StatusBadRequest)
return
}
// Process the order (this is just a placeholder)
w.WriteHeader(http.StatusCreated)
err = json.NewEncoder(w).Encode(order)
if err != nil {
fmt.Println("Error:", err)
return
}
}
func verifyUserID(userID string) bool {
resp, err := http.Get(fmt.Sprintf("http://localhost:8081/verify?id=%s", userID))
if err != nil {
fmt.Println("Error:", err)
return false
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return false
}
var user map[string]interface{}
body, _ := io.ReadAll(resp.Body)
err = json.Unmarshal(body, &user)
if err != nil {
fmt.Println("Error:", err)
return false
}
return user["id"] == userID
}
Run the Order Service
go run main.go
Your Order Service is now running on localhost:8082 and can create orders while verifying user IDs against the User Service.
Testing the Communication
To test the setup, you can use curl or Postman.
Test User Verification
curl http://localhost:8081/verify?id=1
Test Order Creation
Create a JSON file order.json:
{
"id": "101",
"user_id": "1",
"item": "Laptop"
}
Use curl to create an order:
curl -X POST -H "Content-Type: application/json" -d @order.json http://localhost:8082/create
If the user ID is valid, you should receive a 201 Created response with the order details.
But if you use unregistered user_id like 3 you will get like this
Conclusion
You’ve set up two microservices that communicate using REST API calls in Go. The Order Service verifies user IDs by calling the User Service, ensuring that only registered users can create orders. This pattern is common in microservices architecture, providing a scalable and modular approach to building applications.
What’s Next?
In production environments, we cannot guarantee that our services will always be 100% up and running. What happens if our User Service encounters issues or downtime? Naturally, our Order Service will face disruptions too, since it relies on the User Service to verify user IDs when creating orders.
Imagine a scenario where our system is used by 100,000 users. If the User Service is down, the Order Service will fail to create orders, leading to potential loss of 100,000 orders. This translates to a significant financial loss for the company.
To mitigate this risk and make our system more fault-tolerant, we need to implement a backoff retry mechanism. This approach involves retrying failed requests with increasing delays, allowing temporary issues to resolve themselves without overwhelming the services.
We will cover the backoff retry mechanism implementation in the next article.