# 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, ¤cyCode); 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



https://github.com/rich7420/microservices-demo/tree/orderHistoryService
**Tags**: `#microservices` `#kubernetes` `#grpc` `#postgresql` `#docker` `#skaffold` `#golang`