# 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`