Published on

Implementing Simple Microservices with Go and RabbitMQ in eCommerce

Authors

Introduction

Microservices architecture has seen a rapid rise in popularity among developers and businesses alike, due to the flexibility and scalability it provides, as compared to traditional monolithic architectures. In the context of an eCommerce platform, microservices can streamline development, facilitate team collaboration, and simplify the process of scaling or updating individual components.

In this tutorial, we'll walk through the process of creating a microservice-oriented eCommerce platform using Go, RabbitMQ, and Docker. Our platform will consist of two primary services: 'product' and 'order'.

RabbitMQ is an open-source message broker that enables loosely coupled communication between microservices. We'll delve deeper into RabbitMQ's role as we progress.

Before diving into the main content, let's take a look at some prerequisites.

Prerequisites

To follow this tutorial, ensure that you have:

  • Golang installed on your system.
  • Basic understanding of the Go language and syntax.
  • RabbitMQ and Docker installed on your system.

Microservices in Go

Golang, or Go, has become a go-to language for implementing microservices due to its simple syntax, Golang compiles codes faster, strong standard library, and intrinsic support for concurrent programming. Moreover, its compiled nature leads to fast execution and less resource consumption, making it an optimal choice for microservices.

In our eCommerce platform, we'll use Go to build our 'product' and 'order' services. The product service will handle functionalities related to products, such as creating new products or fetching product details. The order service will manage the ordering process, updating the status and details of orders.

To facilitate communication between these services, we'll use RabbitMQ.

RabbitMQ and Inter-Service Communication

RabbitMQ will be used as the message broker in our architecture. It will enable our services to communicate and share data without needing to be directly connected, thus adhering to the loose coupling principle of microservices. When a service needs to communicate an event or data, it sends a message to a queue, and RabbitMQ ensures that the message is delivered to all interested services.

Implementation

Let's begin implementing our microservices.

Firstly, let's structure our project. We will have two main directories, one for each service:

├── order-service
│   ├── main.go
│   ├── order.go
│   ├── dockerfile
└── product-service
    ├── main.go
    ├── product.go
    ├── dockerfile

Product Service

Let's initialize our product service. The product service will handle all operations related to products.

Defining the Product Struct

In product.go, we define our Product struct and methods for creating and retrieving products:

Copy code
package main

import (
	"encoding/json"
	"log"
)

type Product struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	Description string `json:"description"`
	Price       float64 `json:"price"`
}

// Function to create a new product
func createProduct(id string, name string, desc string, price float64) *Product {
	p := Product{ID: id, Name: name, Description: desc, Price: price}

	// Simulate writing to a database
	data, err := json.Marshal(p)
	if
err != nil {
		log.Fatal(err)
	}

	log.Printf("New product created: %s\n", data)

	return &p
}

// Function to fetch a product by id
func getProduct(id string) *Product {
	// Simulate fetching from a database
	// In a real-world application, you'll want to replace this with actual database calls

	p := Product{ID: id, Name: "Sample Product", Description: "This is a sample product.", Price: 19.99}
	return &p
}

the createProduct and getProduct functions represent database calls.

Implementing RabbitMQ Messaging

In main.go, we define our main function where we establish a connection with RabbitMQ, define our routes, and start the server:

Copy code
package main

import (
	"log"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/streadway/amqp"
)

func failOnError(err error, msg string) {
	if err != nil {
		log.Fatalf("%s: %s", msg, err)
	}
}

func main() {
	// Connect to RabbitMQ
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	failOnError(err, "Failed to connect to RabbitMQ")
	defer conn.Close()

	ch, err := conn.Channel()
	failOnError(err, "Failed to open a channel")
	defer ch.Close()

	// Declare a queue for our product-service
	q, err := ch.QueueDeclare(
		"product-service-queue", // name
		false,   // durable
		false,   // delete when unused
		false,   // exclusive
		false,   // no-wait
		nil,     // arguments
	)
	failOnError(err, "Failed to declare a queue")

	// Create a new Gin router
	router := gin.Default()

	// Define our routes
	router.POST("/product", func(c *gin.Context) {
		var p Product

		if err := c.ShouldBindJSON(&p); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		// Create a new product and print its details
		product := createProduct(p.ID, p.Name, p.Description, p.Price)
		c.JSON(http.StatusOK, product)
	})

	router.GET("/product/:id", func(c *gin.Context) {
		// Retrieve the product by id
		product := getProduct(c.Param("id"))
		c.JSON(http.StatusOK, product)
	})

	// Run the server
	router.Run(":8080")
}

In this code, we connect to RabbitMQ, declare a queue, define our routes for creating and fetching products, and then start the server.

Order Service

Onto our order service, the order service will handle operations related to creating and managing orders.

Defining the Order Struct

In order.go, we define our Order struct and methods for creating and retrieving orders:

Copy code
package main

import (
	"encoding/json"
	"log"
)

type Order struct {
	ID       string   `json:"id"`
	ProductIDs []string `json:"product_ids"`
	Status   string `json:"status"`
}

// Function to create a new order
func createOrder(id string, productIDs []string, status string) *Order {
	o := Order{ID: id, ProductIDs: productIDs, Status: status}

	// Simulate writing to a database
	data, err := json.Marshal(o)
	if err != nil {
		log.Fatal(err)
	}

		// ...
	log.Printf("New order created: %s\n", data)

	return &o
}

// Function to fetch an order by id
func getOrder(id string) *Order {
	// Simulate fetching from a database
	// In a real-world application, you'll want to replace this with actual database calls

	o := Order{ID: id, ProductIDs: []string{"product1", "product2"}, Status: "processed"}
	return &o
}

Implementing RabbitMQ Messaging in the Order Service

We now need to integrate RabbitMQ into our order service as well, to ensure communication between our microservices. Similar to the product service, we will create a connection to the RabbitMQ server, create a channel, and define a queue for the order service.

In main.go of the order service, we first connect to RabbitMQ, declare our queue, and then define a function to consume from the product service queue:

Copy code
package main

import (
	"encoding/json"
	"log"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/streadway/amqp"
)

func failOnError(err error, msg string) {
	if err != nil {
		log.Fatalf("%s: %s", msg, err)
	}
}

func main() {
	// Connect to RabbitMQ
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	failOnError(err, "Failed to connect to RabbitMQ")
	defer conn.Close()

	ch, err := conn.Channel()
	failOnError(err, "Failed to open a channel")
	defer ch.Close()

	// Declare a queue for our order-service
	q, err := ch.QueueDeclare(
		"order-service-queue", // name
		false,   // durable
		false,   // delete when unused
		false,   // exclusive
		false,   // no-wait
		nil,     // arguments
	)
	failOnError(err, "Failed to declare a queue")

	// Consume from the product-service-queue and create an order
	msgs, err := ch.Consume(
		"product-service-queue", // queue
		"",     // consumer
		true,   // auto-ack
		false,  // exclusive
		false,  // no-local
		false,  // no-wait
		nil,    // args
	)
	failOnError(err, "Failed to register a consumer")

	go func() {
		for d := range msgs {
			log.Printf("Received a message: %s", d.Body)

			// Unmarshal the product IDs from the received message
			var productIDs []string
			err = json.Unmarshal(d.Body, &productIDs)
			failOnError(err, "Failed to unmarshal JSON")

			// Create a new order
			order := createOrder("1", productIDs, "CREATED")
			log.Printf("Created a new order: %s\n", order)

			// Publish the created order to the order-service-queue
			body, err := json.Marshal(order)
			failOnError(err, "Failed to marshal JSON")

			err = ch.Publish(
				"",     // exchange
				q.Name, // routing key
				false,  // mandatory
				false,  // immediate
				amqp.Publishing{
					ContentType: "application/json",
					Body:        body,
				})
			failOnError(err, "Failed to publish a message")

			log.Printf("Published a message: %s\n", body)
		}
	}()

	log.Printf(" [*] Waiting for messages. To exit press CTRL+C")

	// Create a new Gin router
	router := gin.Default()

	// Define our routes
	router.POST("/order", func(c *gin.Context) {
		var o Order

		if err := c.ShouldBindJSON(&o); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		// Create a new order and print its details
		order := createOrder(o.ID, o.ProductIDs, o.Status)

		c.JSON(http.StatusOK, gin.H{
			"message": "Order created successfully",
			"order":   order,
		})
	})

	// Run the server on port 8081
	router.Run(":8081")
}

In the handler function for the /order route, we first bind the JSON body of the request to an Order struct. If the body cannot be unmarshaled into an Order, we send a 400 Bad Request response back to the client. If the body can be unmarshaled successfully, we create a new order, print its details, and then send a 200 OK response back to the client with the details of the created order.

The Order Model Let's define the Order struct in a separate order.go file:

Copy code
package main

import "time"

// Order represents an order made by a customer
type Order struct {
	ID         string    `json:"id"`
	ProductIDs []string  `json:"product_ids"`
	Status     string    `json:"status"`
	CreatedAt  time.Time `json:"created_at"`
}

// createOrder creates a new order with the given product IDs and status
func createOrder(id string, productIDs []string, status string) Order {
	return Order{
		ID:         id,
		ProductIDs: productIDs,
		Status:     status,
		CreatedAt:  time.Now(),
	}
}

The Order struct has four fields: ID, ProductIDs, Status, and CreatedAt. We also have a function createOrder that takes an ID, a slice of product IDs, and a status, and returns a new Order.

Conclusion

In this post, we went over the basics of microservices and their advantages over monolithic applications. We then built two simple microservices in Go, one for managing products and the other for managing orders. We used RabbitMQ to handle communication between the two services, allowing us to create orders consisting of multiple products.

Thanks for reading. Leave a comment,like and share.

If you enjoyed this article, please leave a comment, like it, share it, and follow me on Twitter @Tonycodh.