Building a Robust Order Service with Go, gRPC, PostgreSQL, and GORM

This article explores the development of a robust Order Service as part of an e-commerce platform. We’ll delve into the implementation details using Golang, gRPC, PostgreSQL, and GORM, providing a comprehensive guide to creating a scalable and efficient service. This service handles order creation and retrieval, integrating seamlessly with a user authentication system.

Project Structure

A well-organized project structure is crucial for maintainability. The Order Service is structured as follows:

ecom-grpc/orderd/
│-- db/
│   │-- db.go
│   │-- order.go
│-- service/
│   │-- service.go
│   │-- create_order.go
│   │-- get_order.go
│-- main.go
│-- .env
│-- Dockerfile
│-- .dockerignore

This structure promotes a clear separation of concerns, with dedicated directories for database interactions (db), service logic (service), and the main application entry point (main.go).

Database Setup: PostgreSQL

The Order Service utilizes PostgreSQL as its persistent data store. PostgreSQL is a powerful, open-source relational database known for its reliability and scalability.

If you don’t have PostgreSQL installed, you can easily run it within a Docker container:

docker run --name postgres-cluster -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres
docker exec -it postgres-cluster psql -U postgres -c "CREATE DATABASE orderdb;"

These commands create a PostgreSQL container named postgres-cluster and initialize a database named orderdb.

Database Interaction Layer (db/)

The db package encapsulates all interactions with the PostgreSQL database.

db/db.go – Database Connection

This file establishes the connection to the PostgreSQL database using GORM, a fantastic ORM library for Go. GORM simplifies database operations by providing a clean and intuitive interface.

package db

import (
    "log"

    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

// Provider defines the interface for database operations.
type Provider interface {
    CreateOrder(order *Order) (*Order, error)
    GetOrderById(id string) (*Order, error)
}

// provider implements the Provider interface.
type provider struct {
    db *gorm.DB
}

// New creates a new database provider and connects to the database.
func New(dbURL string) Provider {
    db, err := gorm.Open(postgres.Open(dbURL), &gorm.Config{})
    if err != nil {
        log.Fatalf("Failed to connect to database: %v", err)
    }

    // Automatically migrate the Order model.
    db.AutoMigrate(&Order{})

    return &provider{db}
}

The New function initializes a GORM connection and performs automatic schema migration for the Order model. The Provider interface defines the contract for database operations, allowing for easier testing and potential swapping of database implementations.

db/order.go – Order Model

This file defines the Order model, representing the structure of an order in the database.

package db

import (
    "github.com/google/uuid"
    "gorm.io/gorm"

   // Assuming 'order' package contains the gRPC generated code.
   //  Replace with your actual path to the order protobuf definitions.
    "your_project/proto/order/v1" // Example path.
)

// Order represents the Order model in the database.
type Order struct {
    ID        string  `gorm:"primaryKey"`
    UserID    string
    Product   string
    Quantity  int32
    UnitPrice float64
}

// AsAPIOrder converts the database Order model to the API Order model.
func (o *Order) AsAPIOrder() *order.Order {
    return &order.Order{
        Id:        o.ID,
        UserId:    o.UserID,
        Product:   o.Product,
        Quantity:  o.Quantity,
        UnitPrice: o.UnitPrice,
    }
}
// BeforeCreate hook to generate a UUID for the order ID.
func (o *Order) BeforeCreate(tx *gorm.DB) (err error) {
    o.ID = uuid.NewString()
    return
}

// CreateOrder creates a new order in the database.
func (p *provider) CreateOrder(o *Order) (*Order, error) {
    err := p.db.Create(o).Error
    return o, err
}

// GetOrderById retrieves an order by its ID.
func (p *provider) GetOrderById(id string) (*Order, error) {
    var o Order
    err := p.db.Where("id = ?", id).First(&o).Error
    return &o, err
}

Key aspects of this file include:

  • UUID Generation: The BeforeCreate hook automatically generates a unique UUID for each order using the github.com/google/uuid package. This ensures globally unique order identifiers.
  • Data Mapping: The AsAPIOrder function converts the database Order model to its corresponding gRPC representation, facilitating data transfer between the service and clients.
  • CRUD Operations: CreateOrder and GetOrderById provide methods for creating and retrieving orders from the database.

Service Logic Layer (service/)

The service package contains the core business logic and implements the gRPC server.

service/service.go – Service Dependencies

This file defines the dependencies and structure of the Order Service.

package service

import (
    "context"
    "errors"

    "google.golang.org/grpc/metadata"

    // Assuming 'order' and 'user' packages contain the gRPC generated code.
    // Replace with your actual path to the protobuf definitions.
    "your_project/proto/order/v1" // Example path
    "your_project/proto/user/v1"  // Example Path

    "your_project/db" // Assuming 'db' is in the root of your project
)

type Config struct {
    UserServiceAddress string
}
type Dependencies struct {
    DBProvider db.Provider
    UserService user.UserServiceClient
}

// Service interface defines the methods exposed by the Order Service.
type Service interface {
    order.OrderServiceServer
}

type service struct {
    Config
    Dependencies
}

// New creates a new instance of the Order Service.
func New(cfg Config, deps Dependencies) Service {
    return &service{
        Config:       cfg,
        Dependencies: deps,
    }
}

// authorize verifies the user's identity using the User Service.
func (s *service) authorize(ctx context.Context) (string, error) {
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return "", errors.New("missing metadata")
    }

    authHeader, exists := md["authorization"]
    if !exists || len(authHeader) == 0 {
        return "", errors.New("missing authorization token")
    }
    token := authHeader[0]

    // Add the token to the outgoing context for the User Service call.
    ctx = metadata.AppendToOutgoingContext(ctx, "authorization", token)

    // Call the User Service to verify the user.
    userResp, err := s.UserService.Me(ctx, &user.MeRequest{})
    if err != nil {
        return "", err
    }
    return userResp.GetUser().GetId(), nil
}

Crucially, this file includes an authorize function. This function extracts the authorization token from the incoming gRPC metadata, forwards it to the User Service, and retrieves the user ID. This ensures that only authenticated users can interact with the Order Service.

service/create_order.go – Create Order API

This file implements the CreateOrder gRPC method.

package service

import (
    "context"
    "errors"

    "your_project/proto/order/v1"
    "your_project/db"
)
// CreateOrder handles the creation of a new order.
func (s *service) CreateOrder(ctx context.Context, req *order.CreateOrderRequest) (*order.CreateOrderResponse, error) {
    // Authorize the user.
    userID, err := s.authorize(ctx)
    if err != nil {
        return nil, err
    }

    // Validate the request.
    product := req.GetProduct()
    quantity := req.GetQuantity()
    if product == "" {
        return nil, errors.New("product is required")
    }
    if quantity <= 0 {
        return nil, errors.New("quantity must be greater than 0")
    }

    //  Static price for demonstration.  In a real-world scenario,
    //  you would fetch the price from a product catalog.
    price := 10.5

    // Create the order in the database.
    resOrder, err := s.DBProvider.CreateOrder(&db.Order{
        UserID:    userID,
        Product:   product,
        Quantity:  quantity,
        UnitPrice: price,
    })
    if err != nil {
        return nil, err
    }

    return &order.CreateOrderResponse{
        Order: resOrder.AsAPIOrder(),
    }, nil
}

This function performs the following steps:

  1. Authorization: Calls the authorize function to verify the user.
  2. Request Validation: Checks for required fields (product) and valid values (quantity).
  3. Order Creation: Uses the DBProvider to create a new order record in the database.
  4. Response: Returns the created order details in the gRPC response format.

service/get_order.go – Get Order API

This file implements the GetOrder gRPC method.

package service

import (
    "context"
    "errors"

    "your_project/proto/order/v1"
)
// GetOrder retrieves an order by its ID.
func (s *service) GetOrder(ctx context.Context, req *order.GetOrderRequest) (*order.GetOrderResponse, error) {
    // Authorize the user.
    userID, err := s.authorize(ctx)
    if err != nil {
        return nil, err
    }

    // Validate the request.
    orderID := req.GetId()
    if orderID == "" {
        return nil, errors.New("order ID is required")
    }

    // Retrieve the order from the database.
    resOrder, err := s.DBProvider.GetOrderById(orderID)
    if err != nil {
        return nil, err
    }

    // Check if the requesting user owns the order.
    if resOrder.UserID != userID {
        return nil, errors.New("unauthorized")
    }

    return &order.GetOrderResponse{
        Order: resOrder.AsAPIOrder(),
    }, nil
}

This function:

  1. Authorization: Verifies the user’s identity.
  2. Request Validation: Checks for a valid order ID.
  3. Order Retrieval: Fetches the order from the database using the DBProvider.
  4. Ownership Check: Ensures that the requesting user is the owner of the order. This is a critical security measure.
  5. Response: Returns the order details in the gRPC response.

Main Application Entry Point (main.go)

main.go is the entry point for the Order Service. It handles:

  • Environment Configuration: Loads environment variables from a .env file.
  • Database Initialization: Creates a new database provider.
  • gRPC Server Setup: Creates a new gRPC server and registers the Order Service implementation.
  • User Service Connection: Establishes a connection to the User Service for authentication.
  • Server Start: Starts the gRPC server and listens for incoming requests.
package main

import (
    "log"
    "net"
    "os"

    "github.com/joho/godotenv"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"

    "your_project/proto/order/v1" // Example path
    "your_project/proto/user/v1"  // Example path

    "your_project/db"       // Assuming 'db' is in the root of your project.
    "your_project/service" // Assuming 'service' is in the root of your project.
)

func main() {
    // Load environment variables from .env file.
    err := godotenv.Load()
    if err != nil {
        log.Println(".env file not found, using environment variables")
    }

    // Get database URL from environment variables.
    dbURL := os.Getenv("DB_URL")
    if dbURL == "" {
        log.Fatal("DB_URL is required")
    }
    // Initialize the database provider.
    dbProvider := db.New(dbURL)

    // Get User Service URL from environment variables.
    userServiceURL := os.Getenv("USER_SERVICE_URL")
    if userServiceURL == "" {
        log.Fatal("USER_SERVICE_URL is required")
    }

    // Create a gRPC connection to the User Service.
    grpcConn, err := grpc.Dial(userServiceURL, grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        log.Fatalf("Failed to dial User Service: %v", err)
    }
    defer grpcConn.Close()

    // Create a User Service client.
    userServiceClient := user.NewUserServiceClient(grpcConn)

    // Create a new gRPC server.
    server := grpc.NewServer()

    // Register the Order Service implementation with the gRPC server.
    orderService := service.New(
        service.Config{},
        service.Dependencies{
            DBProvider:  dbProvider,
            UserService: userServiceClient,
        })
    order.RegisterOrderServiceServer(server, orderService)

    // Start the gRPC server.
    listener, err := net.Listen("tcp", ":50052")
    if err != nil {
        log.Fatalf("Failed to listen: %v", err)
    }
    log.Println("gRPC Server is running on port 50052...")
    if err := server.Serve(listener); err != nil {
        log.Fatalf("Failed to serve: %v", err)
    }
}

Environment Configuration (.env)

The .env file stores configuration settings, such as database credentials and the User Service address:

DB_URL=postgres://postgres:postgres@localhost:5432/orderdb
USER_SERVICE_URL=0.0.0.0:50051

Important: In a production environment, never hardcode sensitive information like database passwords directly in your code. Use environment variables or a secure configuration management system.

Docker Setup

The provided Dockerfile and .dockerignore facilitate containerization of the Order Service.

.dockerignore

/bin
/pkg

This file specifies directories to exclude from the Docker build context, improving build speed.

Dockerfile

FROM golang:1.23 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o orderd ./main.go

FROM alpine:latest
WORKDIR /app
RUN apk add --no-cache ca-certificates
COPY --from=builder /app/orderd .
EXPOSE 50052
CMD ["./orderd"]

This Dockerfile uses a multi-stage build:

  1. Builder Stage: Uses the golang:1.23 image to build the Go application. This stage downloads dependencies and compiles the code.
  2. Runtime Stage: Uses a lightweight alpine:latest image to run the compiled binary. This stage copies the executable from the builder stage and sets the entry point.

Running the Service

You can run the service directly using Go:

go run main.go

Or, you can build and run it using Docker:

docker build -t order-service .
docker run --env-file .env -p 50052:50052 order-service

Testing

To ensure the service works well you should create unit and integration tests , to be sure that every thing is working correctly.

To test manually the service you can use grpcurl:
First replace JWT_TOKEN with a valid token generated from user service.
And ID with a generated id.

grpcurl -plaintext -H "authorization: bearer JWT_TOKEN"  -d '{ "product": "book 1", "quantity": 1  }' -proto=apis/order/v1/order.proto localhost:50052 order.v1.OrderService/CreateOrder

grpcurl -plaintext -H "authorization: bearer JWT_TOKEN"  -d '{ "id": "ID"  }' -proto=apis/order/v1/order.proto localhost:50052 order.v1.OrderService/GetOrder

Conclusion

This guide demonstrated how to build a robust and secure Order Service using Go, gRPC, PostgreSQL, and GORM. Key takeaways include:

  • Clean Architecture: The project structure promotes separation of concerns and maintainability.
  • gRPC for Communication: gRPC provides a high-performance and efficient way for services to communicate.
  • PostgreSQL and GORM: PostgreSQL offers a reliable data store, and GORM simplifies database interactions.
  • Authentication and Authorization: The service integrates with a User Service to ensure secure access and data ownership.
  • Dockerization: The Dockerfile allows for easy deployment and scaling of the service.
  • Testing: You should always test your code.

This comprehensive approach lays the foundation for building a scalable and maintainable e-commerce platform. Future enhancements could include adding features like order cancellation, order status updates, and payment integration.

Innovative Software Technology: Empowering Your E-commerce Solutions

At Innovative Software Technology, we specialize in building high-performance, scalable, and secure e-commerce solutions tailored to your specific needs. Our expertise in technologies like Go, gRPC, PostgreSQL, and GORM enables us to create robust microservices, including order management systems like the one described in this article. We focus on search engine optimization (SEO) for your e-commerce platform to improve organic search visibility, boost website traffic, and increase conversion rates. We implement best practices for keyword research, on-page optimization, technical SEO, and content strategy to ensure your online store ranks highly in search engine results pages (SERPs). Let us help you build a cutting-edge e-commerce platform that drives sales and delivers an exceptional user experience.

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.
You need to agree with the terms to proceed