DEV Community

Rafael Levi Costa
Rafael Levi Costa

Posted on

Microservices Architecture in Golang: A Transformative Experience! 🚀

Introduction: As Steve Jobs, co-founder of Apple, once said, “Technology moves the world.” In this article, I will share my experience with microservices architecture in Golang and how it has revolutionized the way I build scalable and flexible systems. Through a solution implemented for a client, I will explore the concepts and technologies involved, providing valuable insights for those looking to adopt this architecture in their projects. Let’s embark on this journey of microservices empowered by Golang! 💡💻

🌐 Building a REST API in Golang: As highlighted by Martin Fowler, a renowned author in the technology field, in his book “Patterns of Enterprise Application Architecture” “Software architecture is the fundamental organization of a system, embodying its key responsibilities and providing a skeleton for its implementation.” My journey started with building a REST API in Golang, leveraging its advanced concurrency and efficiency features. This API served as a service for the front-end, providing structured data from a MySQL database.

example of code:

package main

import (
 "fmt"
 "log"
 "net/http"
)

func main() {
 http.HandleFunc("/users", getUsersHandler)
 log.Fatal(http.ListenAndServe(":8080", nil))
}

func getUsersHandler(w http.ResponseWriter, r *http.Request) {
 // Handle GET request to fetch users from the database
 fmt.Fprintln(w, "Fetching users...")
}
Enter fullscreen mode Exit fullscreen mode

🔒 Managing Sensitive Data: John Allspaw, a renowned author and software reliability expert, mentions in his book “The Art of Capacity Planning” the importance of protecting sensitive data. In the case of the system I developed, the MySQL database stored user information, including login details, access levels, and service history in a queue. These data were managed through a microservices architecture, where security was a priority. As part of this process, I implemented recommended practices for encryption and key management to ensure the confidentiality of user data.

example of code:

package main

import (
 "crypto/aes"
 "crypto/cipher"
 "crypto/rand"
 "encoding/base64"
 "fmt"
 "io"
)

func encryptData(data []byte, key []byte) ([]byte, error) {
 block, err := aes.NewCipher(key)
 if err != nil {
  return nil, err
 }

 encrypted := make([]byte, aes.BlockSize+len(data))
 iv := encrypted[:aes.BlockSize]
 if _, err := io.ReadFull(rand.Reader, iv); err != nil {
  return nil, err
 }

 stream := cipher.NewCFBEncrypter(block, iv)
 stream.XORKeyStream(encrypted[aes.BlockSize:], data)

 return encrypted, nil
}
Enter fullscreen mode Exit fullscreen mode

💬 Using WebSockets for Real-Time Communication: “Effective communication is the key to any successful relationship” said Tony Hsieh, founder of Zappos. Inspired by this statement, I adopted WebSockets technology to provide real-time communication between the API and employees responsible for service handling. WebSockets enable bidirectional communication with support for real-time events. Events such as queue loading, changing service order, and removing a service were managed by the server using Golang’s native WebSocket library.

example of code:

package main

import (
 "log"
 "net/http"

 "github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{}

func handleWebSocket(w http.ResponseWriter, r *http.Request) {
 conn, err := upgrader.Upgrade(w, r, nil)
 if err != nil {
  log.Println("Failed to upgrade to WebSocket:", err)
  return
 }

 // Handle WebSocket communication and events
 // ...
}
Enter fullscreen mode Exit fullscreen mode

📊 Integrating Unstructured Data with Redis: To handle real-time unstructured data, I sought an agile and efficient solution. Event-Driven Microservices. When dealing with unstructured data, having an efficient caching storage layer is essential. Based on this principle, I integrated the Redis database into the microservices architecture, using it to store and manage data related to services. To ensure the security and efficiency of accessing Redis data, I developed a dedicated service called the Broker, which performed read, write, and delete operations in the Redis queue.

example of code:

package main

import (
 "fmt"

 "github.com/go-redis/redis"
)

func main() {
 redisClient := redis.NewClient(&redis.Options{
  Addr:     "localhost:6379",
  Password: "", // no password set
  DB:       0,  // use default DB
 })

 // Example of writing data to Redis
 err := redisClient.Set("key", "value", 0).Err()
 if err != nil {
  panic(err)
 }

 // Example of reading data from Redis
 val, err := redisClient.Get("key").Result()
 if err != nil {
  panic(err)
 }
 fmt.Println("key:", val)
}
Enter fullscreen mode Exit fullscreen mode

📨 Asynchronous Communication with RabbitMQ: To retrieve service data from a messaging service, I chose RabbitMQ as the asynchronous interface. As stated by Gregor Hohpe, author of “Enterprise Integration Patterns” “Asynchronous communication is an efficient strategy for integrating distributed systems.” Through a service called the consumer, my system connected to RabbitMQ, captured, analyzed, and filtered service requests, forwarding them to the Broker for insertion into the Redis queue. Thus, I established efficient and reliable communication between microservices.

example of code:

package main

import (
 "log"
 "os"
 "os/signal"

 "github.com/streadway/amqp"
)

func main() {
 conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
 if err != nil {
  log.Fatalf("Failed to connect to RabbitMQ: %v", err)
 }
 defer conn.Close()

 ch, err := conn.Channel()
 if err != nil {
  log.Fatalf("Failed to open a channel: %v", err)
 }
 defer ch.Close()

 // Consume messages from RabbitMQ
 msgs, err := ch.Consume(
  "queue_name",
  "",
  true,
  false,
  false,
  false,
  nil,
 )
 if err != nil {
  log.Fatalf("Failed to register a consumer: %v", err)
 }

 forever := make(chan os.Signal, 1)
 signal.Notify(forever, os.Interrupt)

 // Process received messages
 go func() {
  for d := range msgs {
   log.Printf("Received a message: %s", d.Body)
   // Forward the message to the Broker for further processing
  }
 }()

 log.Println("Waiting for messages...")
 <-forever
}
Enter fullscreen mode Exit fullscreen mode

🚀 Enhancing Efficiency with gRPC: “Efficiency is never accidental,” said Elon Musk, founder of Tesla. Inspired by this vision, I opted to use gRPC for communication between microservices. As mentioned by Brendan Burns, one of the creators of Kubernetes, in “Designing Distributed Systems” “gRPC is a high-performance, data-interchange communication framework.” By adopting gRPC, I could accelerate communication between microservices, optimizing performance and efficiency, in contrast to the REST API used for serving the front-end.

example of code:

package main

import (
 "net/http"
 "net/http/httptest"
 "testing"
)

func TestGetUsersHandler(t *testing.T) {
 req, err := http.NewRequest("GET", "/users", nil)
 if err != nil {
  t.Fatal(err)
 }

 rr := httptest.NewRecorder()
 handler := http.HandlerFunc(getUsersHandler)

 handler.ServeHTTP(rr, req)

 if rr.Code != http.StatusOK {
  t.Errorf("Expected status code %v, but got %v", http.StatusOK, rr.Code)
 }

 expected := "Fetching users...\n"
 if rr.Body.String() != expected {
  t.Errorf("Expected response body %v, but got %v", expected, rr.Body.String())
 }
}
Enter fullscreen mode Exit fullscreen mode

🔧 Managing Deployment Lifecycle with ArgoCD: As mentioned by Jez Humble and David Farley in “Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation” an automated deployment cycle is essential to ensure agility and reliability in software development. To manage the deployment lifecycle of services, I utilized ArgoCD. This tool monitored a repository containing YAML configuration files of the services, allowing for automatic deployment to a staging environment whenever a new version was detected.

Here is an example of an ArgoCD Application manifest file:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: my-namespace
spec:
  destination:
    namespace: my-namespace
    server: https://kubernetes.default.svc
  source:
    repoURL: https://github.com/my-repo
    targetRevision: main
    path: my-app
  project: my-project
Enter fullscreen mode Exit fullscreen mode

By defining the desired state of the application in the manifest file, ArgoCD automatically synchronized the deployment with the Kubernetes cluster, ensuring consistent and reliable application updates.

⚙️ Infrastructure as Code with Kubernetes: In the era of cloud computing, managing infrastructure as code has become crucial. Kubernetes, an industry-leading container orchestration platform, allows for declarative management of infrastructure resources. By defining Kubernetes manifests, I could describe the desired state of the system, including deployments, services, ingress, and more.

Here is an example of a Kubernetes Deployment manifest file:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-app
        image: my-registry/my-app:latest
        ports:
        - containerPort: 8080
Enter fullscreen mode Exit fullscreen mode

With Kubernetes, I could easily manage and scale microservices, ensuring high availability and fault tolerance.

Diagram of architecture:

Image description
References:

Fowler, M. (2002). Patterns of Enterprise Application Architecture. Addison-Wesley Professional.
Allspaw, J. (2017), The Art of Capacity Planning: Scaling Web Resources in the Cloud
Gregor H. (2003) Enterprise Integration Patterns
Farley, D.(2010) Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation

Top comments (0)