DEV Community

Lakhan Samani
Lakhan Samani

Posted on

Implementing Order Service with Golang, gRPC and PostgreSQL - Part 4

Introduction

In Part 3 we implemented user service. In this post, we will implement an Order Service using Golang, gRPC, PostgreSQL, and GORM.

Note: We are using static price here for product passed in create order request and we will be connecting to user service via grpc to authorize user.

Project Structure

The folder structure for the service is as follows:

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

Each file serves a specific purpose in maintaining a clean and modular design.

Setting Up the Database

We'll use PostgreSQL as our database. If you don’t have PostgreSQL installed, you can run it using Docker:

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;"
Enter fullscreen mode Exit fullscreen mode

Database Layer (db/)

The db package handles all interactions with PostgreSQL.

db/db.go - Database Connection

package db

import (
    "log"

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

// Provider defines the interface for the database provider
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 new database provider
// connects to db and returns the provider
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)
    }

    // Auto-migrate User model
    db.AutoMigrate(&Order{})

    return &provider{db}
}

Enter fullscreen mode Exit fullscreen mode

db/order.go - Order Model

package db

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

    order "github.com/lakhansamani/ecom-grpc-apis/order/v1"
)

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

// AsAPIOrder converts the Order model to API Order
func (o *Order) AsAPIOrder() *order.Order {
    return &order.Order{
        Id:        o.ID,
        UserId:    o.UserID,
        Product:   o.Product,
        Quantity:  o.Quantity,
        UnitPrice: o.UnitPrice,
    }
}

// Before create
func (o *Order) BeforeCreate(tx *gorm.DB) (err error) {
    // Generate UUID
    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 fetches a order by ID from the database
func (p *provider) GetOrderById(id string) (*Order, error) {
    var o Order
    err := p.db.Where("id = ?", id).First(&o).Error
    return &o, err
}

Enter fullscreen mode Exit fullscreen mode

Service Layer (service/)

This layer implements the gRPC server and business logic.

service/service.go - Service Dependencies

package service

import (
    "context"
    "errors"

    "google.golang.org/grpc/metadata"

    order "github.com/lakhansamani/ecom-grpc-apis/order/v1"
    user "github.com/lakhansamani/ecom-grpc-apis/user/v1"

    "github.com/lakhansamani/ecom-grpc-orderd/db"
)

type Config struct {
    UserServiceAddress string
}

type Dependencies struct {
    // Add dependencies here
    DBProvider db.Provider
    // UserService user.Service
    UserService user.UserServiceClient
}

// Service implements the Order service.
type Service interface {
    order.OrderServiceServer
}

type service struct {
    Config
    Dependencies
}

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

// authorize verifies user using the user service and gets userID
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 token to outgoing context
    ctx = metadata.AppendToOutgoingContext(ctx, "authorization", token)

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

Enter fullscreen mode Exit fullscreen mode

service/create_order.go - Create Order API

package service

import (
    "context"
    "errors"

    order "github.com/lakhansamani/ecom-grpc-apis/order/v1"
    "github.com/lakhansamani/ecom-grpc-orderd/db"
)

// CreateOrder API to create a new order
// Permission: authenticated user
func (s *service) CreateOrder(ctx context.Context, req *order.CreateOrderRequest) (*order.CreateOrderResponse, error) {
    // Authorizer user
    userID, err := s.authorize(ctx)
    if err != nil {
        return nil, err
    }
    // Validate request
    product := req.GetProduct()
    quantity := req.GetQuantity()
    if product == "" {
        return nil, errors.New("product is required")
    }
    if quantity <= 0 {
        return nil, errors.New("quantity should be greater than 0")
    }
    // Static Price
    price := float64(10.5)
    // Save order to 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
}

Enter fullscreen mode Exit fullscreen mode

service/get_order.go - Get Order API

package service

import (
    "context"
    "errors"

    order "github.com/lakhansamani/ecom-grpc-apis/order/v1"
)

// GetOrder API to get order details
// Permission: authenticated user who created the order
func (s *service) GetOrder(ctx context.Context, req *order.GetOrderRequest) (*order.GetOrderResponse, error) {
    // Authorizer user
    userID, err := s.authorize(ctx)
    if err != nil {
        return nil, err
    }
    // Get order from database
    orderID := req.GetId()
    if orderID == "" {
        return nil, errors.New("order id is required")
    }
    resOrder, err := s.DBProvider.GetOrderById(orderID)
    if err != nil {
        return nil, err
    }
    // Check if user is authorized to get the order
    if resOrder.UserID != userID {
        return nil, errors.New("unauthorized")
    }
    return &order.GetOrderResponse{
        Order: resOrder.AsAPIOrder(),
    }, nil
}

Enter fullscreen mode Exit fullscreen mode

Main Entry Point (main.go)

package main

import (
    "log"
    "net"
    "os"

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

    order "github.com/lakhansamani/ecom-grpc-apis/order/v1"
    user "github.com/lakhansamani/ecom-grpc-apis/user/v1"

    "github.com/lakhansamani/ecom-grpc-orderd/db"
    "github.com/lakhansamani/ecom-grpc-orderd/service"
)

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

    // DB URL
    dbURL := os.Getenv("DB_URL")
    if dbURL == "" {
        log.Fatal("DB_URL is required")
    }
    // Initialize database
    dbProvider := db.New(dbURL)

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

    // Create UserServiceClient using grpc
    grpcConn, err := grpc.NewClient(userServiceURL, grpc.WithTransportCredentials(
        insecure.NewCredentials(),
    ))
    if err != nil {
        log.Fatalf("Failed to dial UserService: %v", err)
    }
    defer grpcConn.Close()

    userServiceClient := user.NewUserServiceClient(grpcConn)

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

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

    // Start 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)
    }
}

Enter fullscreen mode Exit fullscreen mode

Environment Configuration (.env)

DB_URL=postgres://postgres:postgres@localhost:5432/orderdb
USER_SERVICE_URL=0.0.0.0:50051
Enter fullscreen mode Exit fullscreen mode

Docker Setup

.dockerignore

/bin
/pkg
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

Running the Service

go run main.go
Enter fullscreen mode Exit fullscreen mode

OR

docker build -t order-service .
docker run --env-file .env -p 50052:50052 order-service
Enter fullscreen mode Exit fullscreen mode

Here are the commands to test it

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
Enter fullscreen mode Exit fullscreen mode

Code Link

Conclusion

  • Built an Order Service with authentication.
  • Used gRPC metadata for authorization.
  • Stored orders in PostgreSQL with GORM.

Next: We will learn Logging & Tracing!

Stay Tuned

Top comments (0)