---
# System prepended metadata

title: Order History Microservice Implementation

---

# Order History Microservice Implementation

> Adding Order History Feature to Online Boutique







## Minimum System Requirements

### Required Resources

Before starting, ensure your system meets these minimum requirements:

- **CPU**: 6 cores
- **RAM**: 8 GB
- **Free Disk Space**: 30 GB

> **Note**: These resources are needed to run Docker Desktop with Kubernetes and all microservices.



## Prerequisites

### Required Tools

#### 1. Docker Desktop with Kubernetes enabled

- Install from [Docker Desktop](https://www.docker.com/products/docker-desktop)
- Enable Kubernetes in Settings → Kubernetes
- Configure at least 6 CPUs and 8GB RAM

#### 2. kubectl (Kubernetes CLI)

- Usually installed with Docker Desktop
- Verify: `kubectl version --client`

#### 3. Skaffold (Build and deployment tool)

**macOS (Homebrew)**:
```bash
brew install skaffold
```

**macOS/Linux (curl)**:
```bash
curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/latest/skaffold-linux-amd64
chmod +x skaffold
sudo mv skaffold /usr/local/bin/
```

**Windows**:
```bash
choco install skaffold
```

Verify: `skaffold version` (requires 2.0.2+)

#### 4. Go (for local development)

- Version 1.23.0 or later
- Verify: `go version`

#### 5. Protocol Buffers Compiler (protoc)

- Required for generating gRPC code
- Install via package manager or [protobuf releases](https://github.com/protocolbuffers/protobuf/releases)



## Service Overview

### Microservices Architecture

- **11 existing microservices** (email, product catalog, cart, checkout, etc.)
- **New microservice**: `orderhistoryservice`
- **Database**: PostgreSQL for persistent storage
- **Communication**: gRPC between services

### Why gRPC?

**gRPC (gRPC Remote Procedure Calls)** is chosen for inter-service communication because:

1. **Performance**: Binary protocol (Protocol Buffers) is faster than JSON
2. **Type Safety**: Strong typing with Protocol Buffers
3. **Streaming**: Supports unary and streaming RPCs
4. **Language Agnostic**: Works across different programming languages
5. **HTTP/2**: Built on HTTP/2 for better multiplexing
6. **Code Generation**: Auto-generates client/server code from `.proto` files

### Microservices Communication Pattern

- **Synchronous**: gRPC for real-time service-to-service calls
- **Service Discovery**: Kubernetes DNS (e.g., `orderhistoryservice:50052`)
- **Health Checks**: gRPC health checks for readiness/liveness probes
- **Error Handling**: gRPC status codes for standardized error responses



## Service Definition

### OrderHistoryService

**Purpose**: Store and retrieve order history for users

**Key Features**:
- Save completed orders to PostgreSQL
- Retrieve order history by user ID
- Display order details (items, prices, tracking info)

**Technology Stack**:
- Language: Go
- Database: PostgreSQL 15
- Protocol: gRPC
- Port: 50052



## gRPC Service Definition

### Protocol Buffer Definition

**File**: `protos/demo.proto` (Modified - **Coded by developer**)

```protobuf
// ------------Order History service------------------

service OrderHistoryService {
    rpc SaveOrder(SaveOrderRequest) returns (Empty) {}
    rpc GetOrderHistory(GetOrderHistoryRequest) returns (GetOrderHistoryResponse) {}
}

message SaveOrderRequest {
    string user_id = 1;
    OrderResult order = 2;
}

message GetOrderHistoryRequest {
    string user_id = 1;
}

message GetOrderHistoryResponse {
    repeated OrderHistoryEntry orders = 1;
}

message OrderHistoryEntry {
    string order_id = 1;
    string created_at = 2;
    repeated OrderHistoryItem items = 3;
    Money total_cost = 4;
    string shipping_tracking_id = 5;
}

message OrderHistoryItem {
    string product_id = 1;
    string product_name = 2;
    string product_picture = 3;
    int32 quantity = 4;
    Money price = 5;
}
```



## Step 1: Service Code Implementation

### 1.1 Create Service Directory Structure

**Created**: `src/orderhistoryservice/`

**Files to create**:
- `main.go` - Main service implementation
- `go.mod` - Go module definition
- `genproto.sh` - Proto generation script
- `Dockerfile` - Container build instructions

### 1.2 Main Service Implementation

**File**: `src/orderhistoryservice/main.go` (**Coded by developer**)

**Key Components**:

1. **Database Connection**: PostgreSQL with retry logic
2. **Schema Initialization**: Creates `orders` and `order_items` tables
3. **gRPC Server**: Implements `OrderHistoryService`
4. **Product Catalog Integration**: Fetches product details

#### Main Service Code

```go
package main

import (
    "context"
    "database/sql"
    "fmt"
    "net"
    "os"
    "time"
    
    _ "github.com/lib/pq"
    "github.com/pkg/errors"
    "github.com/sirupsen/logrus"
    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
    
    pb "github.com/GoogleCloudPlatform/microservices-demo/src/orderhistoryservice/genproto"
    healthpb "google.golang.org/grpc/health/grpc_health_v1"
    
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    "go.opentelemetry.io/otel/propagation"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

const (
    listenPort = "50052"
)

var log *logrus.Logger

func init() {
    log = logrus.New()
    log.Level = logrus.DebugLevel
    log.Formatter = &logrus.JSONFormatter{
        FieldMap: logrus.FieldMap{
            logrus.FieldKeyTime:  "timestamp",
            logrus.FieldKeyLevel: "severity",
            logrus.FieldKeyMsg:   "message",
        },
        TimestampFormat: time.RFC3339Nano,
    }
    log.Out = os.Stdout
}

type orderHistoryService struct {
    pb.UnimplementedOrderHistoryServiceServer
    db                    *sql.DB
    productCatalogSvcAddr string
    productCatalogSvcConn *grpc.ClientConn
}
```

#### Database Initialization

```go
func initDB(db *sql.DB) error {
    createTableSQL := `
    CREATE TABLE IF NOT EXISTS orders (
        id SERIAL PRIMARY KEY,
        order_id VARCHAR(255) NOT NULL UNIQUE,
        user_id VARCHAR(255) NOT NULL,
        created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
        shipping_tracking_id VARCHAR(255),
        total_units BIGINT NOT NULL,
        total_nanos INTEGER NOT NULL,
        currency_code VARCHAR(10) NOT NULL
    );

    CREATE TABLE IF NOT EXISTS order_items (
        id SERIAL PRIMARY KEY,
        order_id VARCHAR(255) NOT NULL,
        product_id VARCHAR(255) NOT NULL,
        product_name VARCHAR(255) NOT NULL,
        product_picture TEXT,
        quantity INTEGER NOT NULL,
        price_units BIGINT NOT NULL,
        price_nanos INTEGER NOT NULL,
        currency_code VARCHAR(10) NOT NULL,
        FOREIGN KEY (order_id) REFERENCES orders(order_id) ON DELETE CASCADE
    );

    CREATE INDEX IF NOT EXISTS idx_orders_user_id ON orders(user_id);
    CREATE INDEX IF NOT EXISTS idx_orders_created_at ON orders(created_at DESC);
    CREATE INDEX IF NOT EXISTS idx_order_items_order_id ON order_items(order_id);
    `
    _, err := db.Exec(createTableSQL)
    return err
}
```

#### SaveOrder RPC Implementation

```go
func (s *orderHistoryService) SaveOrder(ctx context.Context, req *pb.SaveOrderRequest) (*pb.Empty, error) {
    log.Infof("[SaveOrder] user_id=%q order_id=%q", req.UserId, req.Order.GetOrderId())

    tx, err := s.db.Begin()
    if err != nil {
        return nil, status.Errorf(codes.Internal, "failed to begin transaction: %v", err)
    }
    defer tx.Rollback()

    // Calculate total
    totalUnits := int64(0)
    totalNanos := int32(0)
    for _, item := range req.Order.GetItems() {
        itemTotal := item.GetCost().GetUnits()*int64(item.GetItem().GetQuantity()) +
            int64(item.GetCost().GetNanos())*int64(item.GetItem().GetQuantity())/1000000000
        totalUnits += itemTotal
        totalNanos += item.GetCost().GetNanos() * item.GetItem().GetQuantity()
    }
    // Add shipping cost
    if req.Order.GetShippingCost() != nil {
        totalUnits += req.Order.GetShippingCost().GetUnits()
        totalNanos += req.Order.GetShippingCost().GetNanos()
    }

    // Insert order
    _, err = tx.Exec(`
        INSERT INTO orders (order_id, user_id, shipping_tracking_id, total_units, total_nanos, currency_code)
        VALUES ($1, $2, $3, $4, $5, $6)
        ON CONFLICT (order_id) DO NOTHING
    `, req.Order.GetOrderId(), req.UserId, req.Order.GetShippingTrackingId(),
        totalUnits, totalNanos, req.Order.GetShippingCost().GetCurrencyCode())
    if err != nil {
        return nil, status.Errorf(codes.Internal, "failed to insert order: %v", err)
    }

    // Insert order items with product details from product catalog
    for _, item := range req.Order.GetItems() {
        productName := item.GetItem().GetProductId()
        productPicture := ""
        if s.productCatalogSvcConn != nil {
            product, err := pb.NewProductCatalogServiceClient(s.productCatalogSvcConn).GetProduct(ctx, &pb.GetProductRequest{
                Id: item.GetItem().GetProductId(),
            })
            if err == nil {
                productName = product.GetName()
                productPicture = product.GetPicture()
            }
        }

        _, err = tx.Exec(`
            INSERT INTO order_items (order_id, product_id, product_name, product_picture, quantity, price_units, price_nanos, currency_code)
            VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
        `, req.Order.GetOrderId(), item.GetItem().GetProductId(), productName, productPicture,
            item.GetItem().GetQuantity(), item.GetCost().GetUnits(), item.GetCost().GetNanos(),
            item.GetCost().GetCurrencyCode())
        if err != nil {
            return nil, status.Errorf(codes.Internal, "failed to insert order item: %v", err)
        }
    }

    if err = tx.Commit(); err != nil {
        return nil, status.Errorf(codes.Internal, "failed to commit transaction: %v", err)
    }

    log.Infof("Order saved successfully: order_id=%q", req.Order.GetOrderId())
    return &pb.Empty{}, nil
}
```

#### GetOrderHistory RPC Implementation

```go
func (s *orderHistoryService) GetOrderHistory(ctx context.Context, req *pb.GetOrderHistoryRequest) (*pb.GetOrderHistoryResponse, error) {
    log.Infof("[GetOrderHistory] user_id=%q", req.UserId)

    rows, err := s.db.Query(`
        SELECT o.order_id, o.created_at, o.shipping_tracking_id, o.total_units, o.total_nanos, o.currency_code
        FROM orders o
        WHERE o.user_id = $1
        ORDER BY o.created_at DESC
    `, req.UserId)
    if err != nil {
        return nil, status.Errorf(codes.Internal, "failed to query orders: %v", err)
    }
    defer rows.Close()

    var orders []*pb.OrderHistoryEntry
    for rows.Next() {
        var orderID, createdAt, trackingID, currencyCode string
        var totalUnits int64
        var totalNanos int32

        if err := rows.Scan(&orderID, &createdAt, &trackingID, &totalUnits, &totalNanos, &currencyCode); err != nil {
            return nil, status.Errorf(codes.Internal, "failed to scan order: %v", err)
        }

        // Get order items
        itemRows, err := s.db.Query(`
            SELECT product_id, product_name, product_picture, quantity, price_units, price_nanos, currency_code
            FROM order_items
            WHERE order_id = $1
            ORDER BY id
        `, orderID)
        if err != nil {
            return nil, status.Errorf(codes.Internal, "failed to query order items: %v", err)
        }

        var items []*pb.OrderHistoryItem
        for itemRows.Next() {
            var productID, productName, productPicture, itemCurrencyCode string
            var quantity int32
            var priceUnits int64
            var priceNanos int32

            if err := itemRows.Scan(&productID, &productName, &productPicture, &quantity, &priceUnits, &priceNanos, &itemCurrencyCode); err != nil {
                itemRows.Close()
                return nil, status.Errorf(codes.Internal, "failed to scan order item: %v", err)
            }

            items = append(items, &pb.OrderHistoryItem{
                ProductId:     productID,
                ProductName:   productName,
                ProductPicture: productPicture,
                Quantity:      quantity,
                Price: &pb.Money{
                    CurrencyCode: itemCurrencyCode,
                    Units:        priceUnits,
                    Nanos:        priceNanos,
                },
            })
        }
        itemRows.Close()

        orders = append(orders, &pb.OrderHistoryEntry{
            OrderId:            orderID,
            CreatedAt:          createdAt,
            Items:              items,
            TotalCost: &pb.Money{
                CurrencyCode: currencyCode,
                Units:        totalUnits,
                Nanos:        totalNanos,
            },
            ShippingTrackingId: trackingID,
        })
    }

    return &pb.GetOrderHistoryResponse{Orders: orders}, nil
}
```

### 1.3 Go Module Definition

**File**: `src/orderhistoryservice/go.mod` (**Coded by developer**)

```go
module github.com/GoogleCloudPlatform/microservices-demo/src/orderhistoryservice

go 1.23.0

require (
    github.com/lib/pq v1.10.9
    github.com/pkg/errors v0.9.1
    github.com/sirupsen/logrus v1.9.3
    go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0
    go.opentelemetry.io/otel v1.35.0
    go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0
    go.opentelemetry.io/otel/sdk v1.35.0
    google.golang.org/grpc v1.71.0
    google.golang.org/protobuf v1.36.6
)
```

### 1.4 Proto Generation Script

**File**: `src/orderhistoryservice/genproto.sh` (**Coded by developer**)

```bash
#!/bin/bash -eu

PATH=$PATH:$(go env GOPATH)/bin
protodir=../../protos
outdir=./genproto

protoc --proto_path=$protodir --go_out=./$outdir --go_opt=paths=source_relative --go-grpc_out=./$outdir --go-grpc_opt=paths=source_relative $protodir/demo.proto
```

**Generated Files** (Auto-generated by `protoc`):
- `genproto/demo.pb.go` - Protocol buffer message definitions
- `genproto/demo_grpc.pb.go` - gRPC service client and server code

---

## Step 2: Dockerfile

### 2.1 Container Build Instructions

**File**: `src/orderhistoryservice/Dockerfile` (**Coded by developer**)

**Why Multi-Stage Build?**
- **Stage 1 (builder)**: Contains build tools (Go compiler, protoc, etc.)
- **Stage 2 (runtime)**: Only contains the compiled binary and runtime libraries
- **Benefits**: Smaller final image, faster deployments, better security

**Key Points**:
- Multi-stage build (builder + runtime)
- CGO enabled for PostgreSQL driver (required by `lib/pq`)
- Proto generation during build (ensures latest proto code)
- Alpine-based runtime image (minimal size ~5MB base)
- Cross-platform support (linux/amd64, linux/arm64)

#### Builder Stage

```dockerfile
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

FROM --platform=$BUILDPLATFORM golang:1.23.4-alpine@sha256:c23339199a08b0e12032856908589a6d41a0dab141b8b3b21f156fc571a3f1d3 AS builder
ARG TARGETOS
ARG TARGETARCH
WORKDIR /src

# Install PostgreSQL client libraries for CGO and protoc
RUN apk add --no-cache gcc musl-dev postgresql-dev protobuf
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
RUN go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

# restore dependencies
COPY go.mod go.sum* ./
RUN go mod download

COPY . .

# Generate proto files
RUN ./genproto.sh || true

# Skaffold passes in debug-oriented compiler flags
ARG SKAFFOLD_GO_GCFLAGS
RUN CGO_ENABLED=1 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -gcflags="${SKAFFOLD_GO_GCFLAGS}" -o /orderhistoryservice .
```

#### Runtime Stage

```dockerfile
FROM alpine:latest
RUN apk --no-cache add ca-certificates postgresql-libs

WORKDIR /src
COPY --from=builder /orderhistoryservice /src/orderhistoryservice

# Definition of this variable is used by 'skaffold debug' to identify a golang binary.
# Default behavior - a failure prints a stack trace for the current goroutine.
# See https://golang.org/pkg/runtime/
ENV GOTRACEBACK=single

EXPOSE 50052
ENTRYPOINT ["/src/orderhistoryservice"]
```

---

## Step 3: Kubernetes Manifests

### 3.1 Service Deployment Configuration

**File**: `kubernetes-manifests/orderhistoryservice.yaml` (**Coded by developer**)

**Kubernetes Concepts Used**:
- **Deployment**: Manages pod replicas, rolling updates, rollbacks
- **Service**: Provides stable network endpoint (ClusterIP)
- **ServiceAccount**: Identity for pods (for RBAC if needed)
- **ConfigMap/Env**: Configuration via environment variables
- **Probes**: Health checks (readiness/liveness)
- **Resources**: CPU/memory limits and requests

**Components**:
1. `orderhistoryservice` Deployment & Service
2. `postgres-orderhistory` Deployment & Service
3. ServiceAccount for orderhistoryservice

**Why Separate PostgreSQL?**
- **Isolation**: Database runs in separate pod
- **Scalability**: Can scale independently
- **Resource Management**: Different resource limits
- **Lifecycle**: Can restart without affecting app pod

#### OrderHistoryService Deployment

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: orderhistoryservice
  labels:
    app: orderhistoryservice
spec:
  selector:
    matchLabels:
      app: orderhistoryservice
  template:
    metadata:
      labels:
        app: orderhistoryservice
    spec:
      serviceAccountName: orderhistoryservice
      terminationGracePeriodSeconds: 5
      containers:
      - name: server
        image: orderhistoryservice
        ports:
        - name: grpc
          containerPort: 50052
        env:
        - name: PORT
          value: "50052"
        - name: POSTGRES_HOST
          value: "postgres-orderhistory"
        - name: POSTGRES_PORT
          value: "5432"
        - name: POSTGRES_USER
          value: "orderhistory"
        - name: POSTGRES_PASSWORD
          value: "orderhistory"
        - name: POSTGRES_DB
          value: "orderhistory"
        - name: PRODUCT_CATALOG_SERVICE_ADDR
          value: "productcatalogservice:3550"
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 200m
            memory: 256Mi
        readinessProbe:
          initialDelaySeconds: 10
          periodSeconds: 5
          grpc:
            port: 50052
        livenessProbe:
          initialDelaySeconds: 10
          periodSeconds: 10
          grpc:
            port: 50052
```

#### PostgreSQL Deployment

```yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres-orderhistory
  labels:
    app: postgres-orderhistory
spec:
  selector:
    matchLabels:
      app: postgres-orderhistory
  template:
    metadata:
      labels:
        app: postgres-orderhistory
    spec:
      containers:
      - name: postgres
        image: postgres:15-alpine
        ports:
        - containerPort: 5432
        env:
        - name: POSTGRES_USER
          value: "orderhistory"
        - name: POSTGRES_PASSWORD
          value: "orderhistory"
        - name: POSTGRES_DB
          value: "orderhistory"
        resources:
          requests:
            cpu: 100m
            memory: 256Mi
          limits:
            cpu: 200m
            memory: 512Mi
        volumeMounts:
        - mountPath: /var/lib/postgresql/data
          name: postgres-data
        readinessProbe:
          exec:
            command:
            - /bin/sh
            - -c
            - pg_isready -U orderhistory
          initialDelaySeconds: 5
          periodSeconds: 5
        livenessProbe:
          exec:
            command:
            - /bin/sh
            - -c
            - pg_isready -U orderhistory
          initialDelaySeconds: 10
          periodSeconds: 10
      volumes:
      - name: postgres-data
        emptyDir: {}
```

#### Service Definitions

```yaml
---
apiVersion: v1
kind: Service
metadata:
  name: orderhistoryservice
  labels:
    app: orderhistoryservice
spec:
  type: ClusterIP
  selector:
    app: orderhistoryservice
  ports:
  - name: grpc
    port: 50052
    targetPort: 50052
---
apiVersion: v1
kind: Service
metadata:
  name: postgres-orderhistory
  labels:
    app: postgres-orderhistory
spec:
  type: ClusterIP
  selector:
    app: postgres-orderhistory
  ports:
  - name: postgres
    port: 5432
    targetPort: 5432
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: orderhistoryservice
```

### 3.2 Update Kustomization

**File**: `kubernetes-manifests/kustomization.yaml` (**Modified by developer**)

```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
 - adservice.yaml
 - cartservice.yaml
 - checkoutservice.yaml
 - currencyservice.yaml
 - emailservice.yaml
 - frontend.yaml
 - paymentservice.yaml
 - productcatalogservice.yaml
 - recommendationservice.yaml
 - shippingservice.yaml
 - orderhistoryservice.yaml  # Added
```

### 3.3 Update Skaffold Build

**File**: `skaffold.yaml` (**Modified by developer**)

**What to Change**:
Add `orderhistoryservice` to the `artifacts` list in the `build` section.

**Before**:
```yaml
build:
  platforms: ["linux/amd64", "linux/arm64"]
  artifacts:
  - image: emailservice
    context: src/emailservice
  # ... other services ...
  - image: frontend
    context: src/frontend
  - image: adservice
    context: src/adservice
```

**After**:
```yaml
build:
  platforms: ["linux/amd64", "linux/arm64"]
  artifacts:
  - image: emailservice
    context: src/emailservice
  # ... other services ...
  - image: frontend
    context: src/frontend
  - image: adservice
    context: src/adservice
  - image: orderhistoryservice  # Added
    context: src/orderhistoryservice
```

**Explanation**:
- Skaffold uses this configuration to build Docker images for each microservice
- Adding `orderhistoryservice` tells Skaffold to build the new service
- The `context` points to the service directory containing the Dockerfile
- Skaffold will automatically detect and use the Dockerfile in that directory
- When you run `skaffold run`, it will build all services listed in `artifacts`

---

## Step 4: Integration with Existing Services

### 4.1 CheckoutService Integration

**File**: `src/checkoutservice/main.go` (**Modified by developer**)

**Changes**:
1. Add order history service connection
2. Call SaveOrder after successful checkout

#### Add Service Connection

```go
type checkoutService struct {
    pb.UnimplementedCheckoutServiceServer
    
    // ... existing connections ...
    
    orderHistorySvcAddr string
    orderHistorySvcConn *grpc.ClientConn
}

func main() {
    // ... existing code ...
    
    svc := new(checkoutService)
    mustMapEnv(&svc.shippingSvcAddr, "SHIPPING_SERVICE_ADDR")
    mustMapEnv(&svc.productCatalogSvcAddr, "PRODUCT_CATALOG_SERVICE_ADDR")
    mustMapEnv(&svc.cartSvcAddr, "CART_SERVICE_ADDR")
    mustMapEnv(&svc.currencySvcAddr, "CURRENCY_SERVICE_ADDR")
    mustMapEnv(&svc.emailSvcAddr, "EMAIL_SERVICE_ADDR")
    mustMapEnv(&svc.paymentSvcAddr, "PAYMENT_SERVICE_ADDR")
    mustMapEnv(&svc.orderHistorySvcAddr, "ORDER_HISTORY_SERVICE_ADDR")  // Added
    
    mustConnGRPC(ctx, &svc.shippingSvcConn, svc.shippingSvcAddr)
    mustConnGRPC(ctx, &svc.productCatalogSvcConn, svc.productCatalogSvcAddr)
    mustConnGRPC(ctx, &svc.cartSvcConn, svc.cartSvcAddr)
    mustConnGRPC(ctx, &svc.currencySvcConn, svc.currencySvcAddr)
    mustConnGRPC(ctx, &svc.emailSvcConn, svc.emailSvcAddr)
    mustConnGRPC(ctx, &svc.paymentSvcConn, svc.paymentSvcAddr)
    mustConnGRPC(ctx, &svc.orderHistorySvcConn, svc.orderHistorySvcAddr)  // Added
}
```

#### Save Order After Checkout

```go
func (cs *checkoutService) PlaceOrder(ctx context.Context, req *pb.PlaceOrderRequest) (*pb.PlaceOrderResponse, error) {
    // ... existing order processing code ...
    
    orderResult := &pb.OrderResult{
        OrderId:            orderID.String(),
        ShippingTrackingId: shippingTrackingID,
        ShippingCost:       prep.shippingCostLocalized,
        ShippingAddress:    req.Address,
        Items:              prep.orderItems,
    }

    if err := cs.sendOrderConfirmation(ctx, req.Email, orderResult); err != nil {
        log.Warnf("failed to send order confirmation to %q: %+v", req.Email, err)
    } else {
        log.Infof("order confirmation email sent to %q", req.Email)
    }

    // Save order to history
    if err := cs.saveOrderHistory(ctx, req.UserId, orderResult); err != nil {
        log.Warnf("failed to save order history: %+v", err)
    } else {
        log.Infof("order history saved for user %q", req.UserId)
    }

    resp := &pb.PlaceOrderResponse{Order: orderResult}
    return resp, nil
}

func (cs *checkoutService) saveOrderHistory(ctx context.Context, userID string, order *pb.OrderResult) error {
    _, err := pb.NewOrderHistoryServiceClient(cs.orderHistorySvcConn).SaveOrder(ctx, &pb.SaveOrderRequest{
        UserId: userID,
        Order:  order})
    if err != nil {
        return fmt.Errorf("failed to save order history: %+v", err)
    }
    return nil
}
```

### 4.2 Update CheckoutService Environment

**File**: `kubernetes-manifests/checkoutservice.yaml` (**Modified by developer**)

```yaml
env:
  # ... existing env vars ...
  - name: ORDER_HISTORY_SERVICE_ADDR
    value: "orderhistoryservice:50052"
```

### 4.3 Frontend Service Updates

**File**: `src/frontend/main.go` (**Modified by developer**)

**Changes**:
1. Add order history service connection
2. Add route handler for `/order-history`

#### Add Service Connection

```go
type frontendServer struct {
    productCatalogSvcAddr string
    productCatalogSvcConn *grpc.ClientConn
    
    // ... existing connections ...
    
    orderHistorySvcAddr string
    orderHistorySvcConn *grpc.ClientConn
}

func main() {
    // ... existing code ...
    
    mustMapEnv(&svc.orderHistorySvcAddr, "ORDER_HISTORY_SERVICE_ADDR")
    
    mustConnGRPC(ctx, &svc.productCatalogSvcConn, svc.productCatalogSvcAddr)
    // ... other connections ...
    mustConnGRPC(ctx, &svc.orderHistorySvcConn, svc.orderHistorySvcAddr)
    
    // Add route
    r.HandleFunc(baseUrl + "/order-history", svc.orderHistoryHandler).Methods(http.MethodGet, http.MethodHead)
}
```

#### Order History Handler

**File**: `src/frontend/handlers.go` (**Modified by developer**)

```go
func (fe *frontendServer) orderHistoryHandler(w http.ResponseWriter, r *http.Request) {
    log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
    log.WithField("currency", currentCurrency(r)).Info("order history")

    currencies, err := fe.getCurrencies(r.Context())
    if err != nil {
        renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve currencies"), http.StatusInternalServerError)
        return
    }

    // Get order history
    history, err := fe.getOrderHistory(r.Context(), sessionID(r))
    if err != nil {
        log.WithField("error", err).Warn("failed to get order history")
        history = &pb.GetOrderHistoryResponse{Orders: []*pb.OrderHistoryEntry{}}
    }

    // Convert currency for each order
    type orderView struct {
        Order      *pb.OrderHistoryEntry
        TotalPrice *pb.Money
        Items      []struct {
            Item  *pb.OrderHistoryItem
            Price *pb.Money
        }
    }
    orders := make([]orderView, len(history.GetOrders()))

    for i, order := range history.GetOrders() {
        totalPrice, err := fe.convertCurrency(r.Context(), order.GetTotalCost(), currentCurrency(r))
        if err != nil {
            log.WithField("error", err).Warn("failed to convert currency for order")
            totalPrice = order.GetTotalCost()
        }
        items := make([]struct {
            Item  *pb.OrderHistoryItem
            Price *pb.Money
        }, len(order.GetItems()))
        for j, item := range order.GetItems() {
            itemPrice, err := fe.convertCurrency(r.Context(), item.GetPrice(), currentCurrency(r))
            if err != nil {
                itemPrice = item.GetPrice()
            }
            items[j] = struct {
                Item  *pb.OrderHistoryItem
                Price *pb.Money
            }{item, itemPrice}
        }
        orders[i] = orderView{
            Order:      order,
            TotalPrice: totalPrice,
            Items:      items,
        }
    }

    if err := templates.ExecuteTemplate(w, "order-history", injectCommonTemplateData(r, map[string]interface{}{
        "orders":        orders,
        "currencies":    currencies,
        "show_currency": true,
    })); err != nil {
        log.Println(err)
    }
}
```

#### gRPC Client Function

**File**: `src/frontend/rpc.go` (**Modified by developer**)

```go
func (fe *frontendServer) getOrderHistory(ctx context.Context, userID string) (*pb.GetOrderHistoryResponse, error) {
    resp, err := pb.NewOrderHistoryServiceClient(fe.orderHistorySvcConn).GetOrderHistory(ctx, &pb.GetOrderHistoryRequest{
        UserId: userID,
    })
    return resp, errors.Wrap(err, "failed to get order history")
}
```

### 4.4 Order History Template

**File**: `src/frontend/templates/order-history.html` (**Coded by developer**)

**Features**:
- Card-based layout
- Order details with items
- Empty state handling
- Responsive design

```html
{{ define "order-history" }}
    {{ template "header" . }}

    <main role="main" class="order-history">
        <div class="container">
            <div class="row">
                <div class="col-12">
                    <h2 class="order-history-title">Order History</h2>
                </div>
            </div>

            {{ if and $.orders (gt (len $.orders) 0) }}
                {{ range $.orders }}
                <div class="order-history-card">
                    <div class="order-header">
                        <div class="order-header-left">
                            <div class="order-id-label">Order #</div>
                            <div class="order-id-value">{{ .Order.OrderId }}</div>
                            {{ if .Order.ShippingTrackingId }}
                            <div class="order-tracking">
                                <span class="tracking-label">Tracking:</span>
                                <span class="tracking-value">{{ .Order.ShippingTrackingId }}</span>
                            </div>
                            {{ end }}
                        </div>
                        <div class="order-header-right">
                            <div class="order-date">{{ .Order.CreatedAt }}</div>
                        </div>
                    </div>

                    <div class="order-items-section">
                        {{ range .Items }}
                        <div class="order-item-card">
                            <div class="order-item-image-container">
                                {{ if .Item.ProductPicture }}
                                <img src="{{ $.baseUrl }}{{ .Item.ProductPicture }}" alt="{{ .Item.ProductName }}" class="order-item-image">
                                {{ else }}
                                <div class="order-item-placeholder">No Image</div>
                                {{ end }}
                            </div>
                            <div class="order-item-details">
                                <div class="order-item-name">{{ .Item.ProductName }}</div>
                                <div class="order-item-meta">
                                    <span class="order-item-quantity">Qty: {{ .Item.Quantity }}</span>
                                    <span class="order-item-separator">•</span>
                                    <span class="order-item-id">ID: {{ .Item.ProductId }}</span>
                                </div>
                            </div>
                            <div class="order-item-price">
                                {{ renderMoney .Price }}
                            </div>
                        </div>
                        {{ end }}
                    </div>

                    <div class="order-footer">
                        <div class="order-total-label">Total Paid</div>
                        <div class="order-total-value">{{ renderMoney .TotalPrice }}</div>
                    </div>
                </div>
                {{ end }}
            {{ else }}
                <div class="order-history-empty">
                    <div class="empty-state-icon">📦</div>
                    <h3>No order history found</h3>
                    <p>You haven't placed any orders yet.</p>
                    <a class="cymbal-button-primary" href="{{ $.baseUrl }}/" role="button">
                        Continue Shopping
                    </a>
                </div>
            {{ end }}

            <div class="row padding-y-24">
                <div class="col-12 text-center">
                    <a class="cymbal-button-secondary" href="{{ $.baseUrl }}/" role="button">
                        ← Back to Home
                    </a>
                </div>
            </div>
        </div>
    </main>

    {{ template "footer" . }}
{{ end }}
```

### 4.5 Add Navigation Button

**File**: `src/frontend/templates/header.html` (**Modified by developer**)

```html
{{ if $.assistant_enabled }}
<a href="{{ $.baseUrl }}/assistant" class="cart-link">
  <img src="{{ $.baseUrl }}/static/icons/Hipster_WandIcon.svg" style="width: 22px; height: 22px;" alt="Assistant icon" class="logo" title="Assistant" />
</a>
{{ end }}

<a href="{{ $.baseUrl }}/order-history" class="cart-link">
    <img src="{{ $.baseUrl }}/static/icons/Hipster_CheckOutIcon.svg" alt="Order History icon" class="logo" title="Order History" />
</a>

<a href="{{ $.baseUrl }}/cart" class="cart-link">
    <img src="{{ $.baseUrl }}/static/icons/Hipster_CartIcon.svg" alt="Cart icon" class="logo" title="Cart" />
    {{ if $.cart_size }}
    <span class="cart-size-circle">{{$.cart_size}}</span>
    {{ end }}
</a>
```

### 4.6 Order History Styles

**File**: `src/frontend/static/styles/order-history.css` (**Coded by developer**)

**File**: `src/frontend/templates/header.html` (**Modified by developer** - Added CSS link)

```html
<link rel="stylesheet" type="text/css" href="{{ $.baseUrl }}/static/styles/order-history.css">
```

**Key Styles**:
- Card-based design with shadows
- Responsive layout
- Clean typography
- Empty state styling

---

## gRPC Communication Flow

### Service-to-Service Communication

**Communication Patterns**:

1. **Frontend → CheckoutService** (HTTP)
   - User places order via HTTP POST to `/cart/checkout`
   - Frontend calls CheckoutService.PlaceOrder via gRPC

2. **CheckoutService → OrderHistoryService** (gRPC)
   - After successful order, CheckoutService calls `SaveOrder`
   - Sends order details (user_id, order_id, items, total)

3. **OrderHistoryService → PostgreSQL** (SQL)
   - Saves order to `orders` table
   - Saves order items to `order_items` table
   - Fetches product details from ProductCatalogService

4. **Frontend → OrderHistoryService** (gRPC)
   - User navigates to `/order-history` page
   - Frontend calls `GetOrderHistory` with user session ID
   - Returns list of orders with items and prices

### Key Points

- All inter-service communication uses **gRPC**
- Frontend uses **HTTP** for user requests
- OrderHistoryService uses **SQL** for database operations

---

## gRPC Implementation Details

### Protocol Buffer Code Generation

**What are Protocol Buffers?**
- Language-neutral, platform-neutral serialization format
- Define data structures and services in `.proto` files
- Compile to language-specific code (Go, Java, Python, etc.)
- More efficient than JSON (smaller size, faster parsing)

**Code Generation Process**:
1. **Define** service in `protos/demo.proto` (**Coded by developer**)
   - Define RPC methods
   - Define message types (requests/responses)
   
2. **Generate** Go code using `protoc` (**Auto-generated**)
   ```bash
   protoc --go_out=./genproto --go-grpc_out=./genproto demo.proto
   ```
   
3. **Generated Files**:
   - `demo.pb.go` - Message types (structs for requests/responses)
   - `demo_grpc.pb.go` - Client/Server interfaces (gRPC stubs)

**Generated Code Location**:
- `src/orderhistoryservice/genproto/` (**Auto-generated**)
- `src/checkoutservice/genproto/` (**Auto-generated**)
- `src/frontend/genproto/` (**Auto-generated**)

**Why Auto-Generate?**
- Ensures type safety between client and server
- Reduces boilerplate code
- Keeps client/server in sync with proto definition

### gRPC Client Implementation

**Code**: `src/checkoutservice/main.go` (**Coded by developer**)

```go
// Connection setup
mustConnGRPC(ctx, &svc.orderHistorySvcConn, svc.orderHistorySvcAddr)

// RPC call
func (cs *checkoutService) saveOrderHistory(ctx context.Context, userID string, order *pb.OrderResult) error {
    _, err := pb.NewOrderHistoryServiceClient(cs.orderHistorySvcConn).SaveOrder(ctx, &pb.SaveOrderRequest{
        UserId: userID,
        Order:  order})
    return err
}
```

**Generated Client Code** (from `demo_grpc.pb.go`):
```go
type OrderHistoryServiceClient interface {
    SaveOrder(ctx context.Context, in *SaveOrderRequest, opts ...grpc.CallOption) (*Empty, error)
    GetOrderHistory(ctx context.Context, in *GetOrderHistoryRequest, opts ...grpc.CallOption) (*GetOrderHistoryResponse, error)
}
```

### gRPC Server Implementation

**Code**: `src/orderhistoryservice/main.go` (**Coded by developer**)

```go
type orderHistoryService struct {
    pb.UnimplementedOrderHistoryServiceServer
    db                    *sql.DB
    productCatalogSvcAddr string
    productCatalogSvcConn *grpc.ClientConn
}

// Implement SaveOrder
func (s *orderHistoryService) SaveOrder(ctx context.Context, req *pb.SaveOrderRequest) (*pb.Empty, error) {
    // ... implementation ...
}

// Implement GetOrderHistory
func (s *orderHistoryService) GetOrderHistory(ctx context.Context, req *pb.GetOrderHistoryRequest) (*pb.GetOrderHistoryResponse, error) {
    // ... implementation ...
}

// Register server
pb.RegisterOrderHistoryServiceServer(srv, svc)
healthpb.RegisterHealthServer(srv, svc)
```

---

## File Generation Summary

### Files Created/Modified

#### **Coded by Developer** (Service Files):
- `protos/demo.proto` - Service definition
- `src/orderhistoryservice/main.go` - Service implementation
- `src/orderhistoryservice/go.mod` - Dependencies
- `src/orderhistoryservice/genproto.sh` - Proto generation script
- `src/orderhistoryservice/Dockerfile` - Container build
- `kubernetes-manifests/orderhistoryservice.yaml` - K8s manifests

#### **Coded by Developer** (Integration Files):
- `src/checkoutservice/main.go` - Integration code
- `src/frontend/main.go` - Frontend integration
- `src/frontend/handlers.go` - HTTP handler
- `src/frontend/rpc.go` - gRPC client
- `src/frontend/templates/order-history.html` - UI template
- `src/frontend/templates/header.html` - Navigation update
- `src/frontend/static/styles/order-history.css` - Styling
- `skaffold.yaml` - Build configuration
- `kubernetes-manifests/kustomization.yaml` - K8s resources
- `kubernetes-manifests/checkoutservice.yaml` - Env var update

#### **Auto-generated Files**:

**Auto-generated by protoc**:
- `src/orderhistoryservice/genproto/demo.pb.go`
- `src/orderhistoryservice/genproto/demo_grpc.pb.go`
- `src/checkoutservice/genproto/demo.pb.go` (regenerated)
- `src/checkoutservice/genproto/demo_grpc.pb.go` (regenerated)
- `src/frontend/genproto/demo.pb.go` (regenerated)
- `src/frontend/genproto/demo_grpc.pb.go` (regenerated)

**Auto-generated by go mod**:
- `src/orderhistoryservice/go.sum` - Dependency checksums

---

## Deployment Steps

### Step 1: Update Proto Definition

- Add OrderHistoryService to `protos/demo.proto`
- Define RPCs and messages

### Step 2: Generate Proto Code

- Run `genproto.sh` in `src/orderhistoryservice/`
- Run `genproto.sh` in `src/checkoutservice/` (regenerate)
- Run `genproto.sh` in `src/frontend/` (regenerate)
- This generates Go code from `.proto` files

### Step 3: Create Service Code

- Implement `src/orderhistoryservice/main.go`
- Create `go.mod` and dependencies
- Implement SaveOrder and GetOrderHistory RPCs

### Step 4: Create Dockerfile

- Multi-stage build for `orderhistoryservice`
- Includes proto generation and CGO support

### Step 5: Create K8s Manifests

- Create `kubernetes-manifests/orderhistoryservice.yaml`
- Define Deployment, Service, and PostgreSQL

### Step 6: Update Existing Services

- Modify `src/checkoutservice/main.go` to call OrderHistoryService
- Modify `src/frontend/main.go` and handlers for UI
- Update `kubernetes-manifests/checkoutservice.yaml` with env vars

### Step 7: Update Build Config

- Add to `skaffold.yaml` artifacts list
- Add to `kubernetes-manifests/kustomization.yaml`

### Step 8: Deploy with Skaffold

```bash
# Build and deploy all services
skaffold run

# Or use dev mode for auto-rebuild
skaffold dev
```

### What Skaffold Does

- **Builds** Docker images for all services in `artifacts`
- **Tags** images with git commit hash
- **Deploys** to Kubernetes using manifests in `kubernetes-manifests/`
- **Monitors** for changes (in dev mode)

---

## Testing and Verification

### 1. Check Deployment Status

```bash
# Check all pods are running
kubectl get pods

# Check services are created
kubectl get services | grep orderhistory

# Check specific pod status
kubectl get pod -l app=orderhistoryservice
```

### 2. View Service Logs

```bash
# View orderhistoryservice logs
kubectl logs -l app=orderhistoryservice --tail=50

# View postgres logs
kubectl logs -l app=postgres-orderhistory --tail=50

# Follow logs in real-time
kubectl logs -f -l app=orderhistoryservice
```

### 3. Test gRPC Communication

```bash
# Port-forward to test locally
kubectl port-forward svc/orderhistoryservice 50052:50052

# Use grpcurl to test (if installed)
grpcurl -plaintext localhost:50052 list
grpcurl -plaintext localhost:50052 hipstershop.OrderHistoryService/GetOrderHistory
```

### 4. Test UI

- Access frontend: `kubectl port-forward svc/frontend 8080:8080`
- Open browser: `http://localhost:8080`
- Navigate to Order History page
- Place a test order
- Verify order appears in history

### 5. Test Complete Flow

1. Add items to cart
2. Checkout and place order
3. Verify order saved (check logs)
4. Navigate to `/order-history`
5. Verify order details displayed correctly

### 6. Database Verification

```bash
# Connect to PostgreSQL pod
kubectl exec -it <postgres-pod-name> -- psql -U orderhistory -d orderhistory

# Check tables
\dt

# Query orders
SELECT * FROM orders LIMIT 5;
SELECT * FROM order_items LIMIT 5;
```

---

## Summary

### What We Built

✅ **New Microservice**: OrderHistoryService with PostgreSQL  
✅ **gRPC Integration**: CheckoutService → OrderHistoryService  
✅ **Frontend Integration**: Order history page with UI  
✅ **Database Schema**: Orders and order_items tables  
✅ **Kubernetes Deployment**: Full K8s manifests  
✅ **UI Improvements**: Modern card-based design  

**Total Services**: 12 microservices (11 existing + 1 new)


# Result





![image](https://hackmd.io/_uploads/rk3gVo3xWe.png)
![image](https://hackmd.io/_uploads/SyuGNo3l-x.png)
![image](https://hackmd.io/_uploads/BJdXEsnxZg.png)

https://github.com/rich7420/microservices-demo/tree/orderHistoryService

**Tags**: `#microservices` `#kubernetes` `#grpc` `#postgresql` `#docker` `#skaffold` `#golang`
