Introduction:
In microservice architectures, it’s crucial to have unique identifiers for the entities within your system. Traditionally, primary keys are used to ensure uniqueness, but they can have some limitations in distributed systems. In this article, we’ll explore how to use Nanoid, a tiny, secure, URL-friendly, unique string ID generator, as a primary key in a Go-based microservice system using the Gin framework. We’ll go through the process of setting up a simple microservice and generating unique IDs with Nanoid.
Why Nanoid is a Better Choice than Auto Increment and UUID
When designing a microservice system, choosing the right method for generating unique identifiers is crucial. In this section, we will discuss why Nanoid is a better choice compared to auto-incrementing integers and UUIDs.
Scalability and Distributed Systems:
In a microservice architecture, multiple instances of a service may run concurrently, leading to potential conflicts with auto-incrementing integers as primary keys. A centralized sequence generator is required to guarantee uniqueness, which creates a single point of failure and hampers scalability. Nanoid, on the other hand, generates unique identifiers without the need for centralized coordination, making it a more suitable option for distributed systems.
UUIDs also provide globally unique identifiers, but they have some drawbacks compared to Nanoid, as discussed below.
Size and URL-friendliness:
Nanoid generates shorter and URL-friendly identifiers. UUIDs are typically 36 characters long, containing non-URL-friendly characters like hyphens. Nanoid IDs can be much shorter (e.g., 10–21 characters) while maintaining a low probability of collisions, making them more suitable for use in URLs or other user-facing identifiers.
Customizable Length and Alphabet:
Nanoid allows you to customize the length and alphabet of the generated IDs. This flexibility enables you to optimize the trade-off between ID length and collision probability depending on your specific use case. UUIDs have a fixed length and structure, which can be unnecessarily long or restrictive for some applications.
Performance:
Nanoid is designed to be fast and efficient. While UUID generation is generally fast, creating a large number of UUIDs in a short period can put pressure on the system’s random number generator, causing performance issues. In contrast, Nanoid relies on a custom algorithm that balances randomness and performance.
In conclusion, Nanoid offers several advantages over auto-incrementing integers and UUIDs when it comes to generating unique identifiers in a microservice system. Its scalability, URL-friendliness, customizability, and performance make it a better choice for modern distributed systems.
Prerequisites:
To follow this tutorial, you’ll need:
A basic understanding of Go programming language
Go installed on your system (version 1.17 or later)
Familiarity with the Gin web framework
Step 1: Installing the required packages
First, let’s install the required packages: Gin and Nanoid. Run the following command in your terminal:
go get -u github.com/gin-gonic/gin
go get -u github.com/matoous/go-nanoid/v2
Step 2: Creating a User model
We’ll create a simple User model to demonstrate the usage of Nanoid as a primary key. Create a new file named models.go
and add the following code:
package main
type User struct {
ID string `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
}
Step 3: Setting up the Gin router and creating a user
Now, we’ll set up the Gin router and create an endpoint to create a new user. Create a new file named main.go
and add the following code:
package main
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/matoous/go-nanoid/v2"
)
func main() {
router := gin.Default()
router.POST("/users", createUser)
log.Fatal(router.Run(":8080"))
}
func createUser(c *gin.Context) {
var newUser User
err := c.BindJSON(&newUser)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
id, err := gonanoid.New(10)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error generating unique ID"})
return
}
newUser.ID = id
c.JSON(http.StatusOK, newUser)
}
In this code, we:
Import the necessary packages (Gin and Nanoid).
Set up the Gin router with a default configuration.
Define a route for creating users with a POST request to “/users”.
Bind the incoming JSON payload to a new User struct.
Generate a unique ID using Nanoid and assign it to the user.
Return the created user with the generated ID.
Step 4: Testing the microservice
Now, let’s test our microservice by running the main.go
file:
go run main.go
This command will start the server on port 8080. You can test the endpoint by making a POST request to http://localhost:8080/users with the following JSON payload:
{
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com"
}
You can use a tool like Postman or curl to send the POST request. If everything works as expected, you should receive a JSON response with the created user and a unique ID generated by Nanoid:
{
"id": "Nl6nhZw1gT",
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com"
}
Step 5: Storing users in-memory
To make our microservice more useful, let’s store the created users in-memory. We’ll use a simple map for this purpose. Update the main.go
file as follows :
package main
import (
"log"
"net/http"
"sync"
"github.com/gin-gonic/gin"
"github.com/matoous/go-nanoid/v2"
)
var (
usersStore = make(map[string]User)
storeMutex = &sync.Mutex{}
)
func main() {
router := gin.Default()
router.POST("/users", createUser)
router.GET("/users/:id", getUser)
log.Fatal(router.Run(":8080"))
}
func createUser(c *gin.Context) {
var newUser User
err := c.BindJSON(&newUser)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
id, err := gonanoid.New(10)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error generating unique ID"})
return
}
newUser.ID = id
storeMutex.Lock()
usersStore[id] = newUser
storeMutex.Unlock()
c.JSON(http.StatusOK, newUser)
}
func getUser(c *gin.Context) {
id := c.Param("id")
storeMutex.Lock()
user, ok := usersStore[id]
storeMutex.Unlock()
if !ok {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
c.JSON(http.StatusOK, user)
}
In this updated version, we:
Define a global map called
usersStore
to store the users and async.Mutex
to handle concurrent access.Add a new route for fetching a user by ID with a GET request to “/users/:id”.
In the
createUser
function, store the newly created user in theusersStore
map.Implement the
getUser
function to fetch a user by ID from theusersStore
map.
Step 6: Testing the updated microservice
Restart your server by running the main.go
file:
go run main.go
Create a user by making a POST request to http://localhost:8080/users with the JSON payload:
{
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com"
}
Copy the generated ID from the response and make a GET request to http://localhost:8080/users/{id}, replacing “{id}” with the actual ID. You should receive the user details in the response.
Conclusion:
In this tutorial, we demonstrated how to use Nanoid as a primary key in a Go-based microservice system with the Gin framework. We created a simple microservice that allows creating and fetching users with unique IDs generated by Nanoid. This approach ensures that our microservice can handle distributed systems efficiently, as Nanoid provides URL-friendly, secure, and unique identifiers.