Building a Go-Based Microservice for Shopping Cart Management

Table of Contents

  1. Introduction
  2. Prerequisites
  3. Setting Up the Project
  4. Creating the Shopping Cart Service
  5. Implementing Cart Operations
  6. Handling Concurrency
  7. Testing the Service
  8. Conclusion

Introduction

In this tutorial, we will build a microservice in Go that enables managing a shopping cart. The microservice will provide functionalities like adding items to the cart, removing items, updating quantities, and retrieving cart details. We will explore the basics of Go programming, best practices for building microservices, and design patterns for scalability and maintainability.

By the end of this tutorial, you will have a working Go-based shopping cart microservice that can be integrated into a larger e-commerce system.

Prerequisites

To follow along with this tutorial, you should have a basic understanding of the Go programming language. Familiarity with concepts like variables, functions, and structs will be helpful. You should also have Go installed on your machine. You can download and install Go from the official Go website.

Setting Up the Project

  1. Create a new directory for your project. Open a terminal and navigate to the desired location.
     mkdir shopping-cart-microservice
     cd shopping-cart-microservice
    
  2. Initialize a new Go module. This ensures that our project has its own isolated dependencies.
     go mod init github.com/your-username/shopping-cart-microservice
    
  3. Create a new main Go file named main.go.
     touch main.go
    
  4. Open the main.go file in a text editor and add the following code to start the basic structure of our microservice.

     package main
        
     import (
     	"fmt"
     	"log"
     	"net/http"
     )
        
     func main() {
     	fmt.Println("Starting Shopping Cart Microservice...")
        
     	// REST API endpoints will be defined here
        
     	log.Fatal(http.ListenAndServe(":8080", nil))
     }
    

    The code sets up a minimal HTTP server that listens on port 8080. It also prints a message to indicate that the service has started successfully.

Creating the Shopping Cart Service

Now let’s create the necessary components for our shopping cart service.

  1. Create a new file named cart.go to define the Cart struct and its associated methods.

     package main
        
     type Cart struct {
     	ID       string
     	Items    []CartItem
     	Total    float64
     	Currency string
     }
        
     type CartItem struct {
     	ID       string
     	Name     string
     	Price    float64
     	Quantity int
     }
    

    Here, we define the Cart struct to represent a shopping cart. It contains an ID, a slice of CartItem structs, the total price, and the currency.

  2. Add a method to the Cart struct to calculate the total price.

     func (c *Cart) CalculateTotalPrice() {
     	var total float64
     	for _, item := range c.Items {
     		total += item.Price * float64(item.Quantity)
     	}
     	c.Total = total
     }
    

    The CalculateTotalPrice method iterates over the cart items and calculates the total price by multiplying the price of each item by its quantity. It updates the Total field of the Cart struct.

Implementing Cart Operations

Next, let’s implement the operations to manage the shopping cart, such as adding items, updating quantities, and retrieving details.

  1. Create a new file named handlers.go to define the request handlers for the different cart operations.

     package main
        
     import (
     	"encoding/json"
     	"net/http"
     )
        
     func AddToCart(w http.ResponseWriter, r *http.Request) {
     	// Parse request body
     	var item CartItem
     	err := json.NewDecoder(r.Body).Decode(&item)
     	if err != nil {
     		http.Error(w, err.Error(), http.StatusBadRequest)
     		return
     	}
        
     	// Add item to cart
     	cart := GetCartFromRequest(r)
     	cart.Items = append(cart.Items, item)
     	cart.CalculateTotalPrice()
        
     	// Return success response
     	w.WriteHeader(http.StatusCreated)
     }
        
     func UpdateCartItem(w http.ResponseWriter, r *http.Request) {
     	// Parse request body
     	var item CartItem
     	err := json.NewDecoder(r.Body).Decode(&item)
     	if err != nil {
     		http.Error(w, err.Error(), http.StatusBadRequest)
     		return
     	}
        
     	// Update item quantity in cart
     	cart := GetCartFromRequest(r)
     	for i, existingItem := range cart.Items {
     		if existingItem.ID == item.ID {
     			cart.Items[i].Quantity = item.Quantity
     			cart.CalculateTotalPrice()
     			w.WriteHeader(http.StatusOK)
     			return
     		}
     	}
        
     	// Item not found in cart
     	http.Error(w, "Item not found in cart", http.StatusBadRequest)
     }
        
     func GetCart(w http.ResponseWriter, r *http.Request) {
     	// Get cart from request context
     	cart := GetCartFromRequest(r)
        
     	// Return cart as JSON response
     	w.Header().Set("Content-Type", "application/json")
     	json.NewEncoder(w).Encode(cart)
     }
        
     // Helper function to get cart from request context
     func GetCartFromRequest(r *http.Request) *Cart {
     	// In a real-world scenario, you would implement code to fetch cart from a database
     	// For simplicity, we will store the cart in the request context
        
     	// Check if cart already exists for this request
     	if val := r.Context().Value("cart"); val != nil {
     		return val.(*Cart)
     	}
        
     	// Create a new cart and store it in the request context
     	cart := &Cart{ID: "user123", Currency: "USD"}
     	ctx := context.WithValue(r.Context(), "cart", cart)
     	return cart
     }
    

    The code defines three request handlers AddToCart, UpdateCartItem, and GetCart. The AddToCart handler parses the request body to obtain the item details, adds the item to the cart, and recalculates the total price. The UpdateCartItem handler updates the quantity of an existing item in the cart based on the item ID. The GetCart handler retrieves the cart from the request context and returns it as a JSON response.

  2. In the main.go file, register the request handlers and start the HTTP server.

     package main
        
     import (
     	"context"
     	"encoding/json"
     	"fmt"
     	"log"
     	"net/http"
     )
        
     func main() {
     	fmt.Println("Starting Shopping Cart Microservice...")
        
     	// Register request handlers
     	http.HandleFunc("/add", AddToCart)
     	http.HandleFunc("/update", UpdateCartItem)
     	http.HandleFunc("/cart", GetCart)
        
     	log.Fatal(http.ListenAndServe(":8080", nil))
     }
    

Handling Concurrency

To ensure the shopping cart microservice can handle concurrent requests and updates correctly, we need to add synchronization mechanisms.

  1. Modify the Cart struct in cart.go to include a mutex.

     package main
        
     import (
     	"sync"
     )
        
     type Cart struct {
     	ID       string
     	Items    []CartItem
     	Total    float64
     	Currency string
     	sync.Mutex
     }
    

    The sync.Mutex type is used for mutual exclusion, allowing only one goroutine to access the cart at a time.

  2. Update the Cart methods in cart.go to lock and unlock the mutex appropriately.

     func (c *Cart) CalculateTotalPrice() {
     	c.Lock()
     	defer c.Unlock()
        
     	var total float64
     	for _, item := range c.Items {
     		total += item.Price * float64(item.Quantity)
     	}
     	c.Total = total
     }
    
     func (c *Cart) AddItem(item CartItem) {
     	c.Lock()
     	defer c.Unlock()
        
     	c.Items = append(c.Items, item)
     	c.CalculateTotalPrice()
     }
    
     func (c *Cart) UpdateItemQuantity(item CartItem) {
     	c.Lock()
     	defer c.Unlock()
        
     	for i, existingItem := range c.Items {
     		if existingItem.ID == item.ID {
     			c.Items[i].Quantity = item.Quantity
     			c.CalculateTotalPrice()
     			return
     		}
     	}
     }
        
     // Other methods in the Cart struct should also lock/unlock as appropriate
    

    By locking and unlocking the mutex, we ensure that only one goroutine can modify the cart at a time, preventing potential race conditions.

Testing the Service

To test the shopping cart microservice, we can use Go’s built-in testing framework.

  1. Create a new file named handlers_test.go to define the unit tests for the request handlers.

     package main
        
     import (
     	"bytes"
     	"encoding/json"
     	"net/http"
     	"net/http/httptest"
     	"testing"
     )
        
     func TestAddToCart(t *testing.T) {
     	// Create a new request with a sample item JSON
     	item := CartItem{
     		ID:       "item1",
     		Name:     "Item 1",
     		Price:    9.99,
     		Quantity: 1,
     	}
     	itemJSON, _ := json.Marshal(item)
     	req, err := http.NewRequest("POST", "/add", bytes.NewBuffer(itemJSON))
     	if err != nil {
     		t.Fatal(err)
     	}
        
     	// Create a response recorder
     	rr := httptest.NewRecorder()
        
     	// Call the AddToCart handler function
     	handler := http.HandlerFunc(AddToCart)
     	handler.ServeHTTP(rr, req)
        
     	// Check the response status code
     	if status := rr.Code; status != http.StatusCreated {
     		t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusCreated)
     	}
        
     	// TODO: Check other assertions for verifying cart content and total price
     	// (e.g., assert cart contains the added item and has the correct total price)
     }
        
     // Similar unit tests for other request handlers
    
  2. Run the unit tests using the go test command.

     go test
    

    The test code sends a sample item JSON to the AddToCart handler and verifies the response status code. You can add additional assertions to test the correctness of the cart content and the total price.

Conclusion

In this tutorial, we built a Go-based microservice for shopping cart management. We covered the basics of Go programming, implemented cart operations, handled concurrency using mutexes, and tested the service using Go’s testing framework. By following this tutorial, you should now have a solid foundation for building scalable and maintainable microservices in Go.

Remember to explore more Go features, design patterns, and best practices to enhance your microservice further. Happy coding!