Skip to main content

Overview

The Greeter service is a Go-based microservice that provides greeting functionality while demonstrating service-to-service communication patterns. It calls the Caller service to fetch external API data and stores greeting logs in PostgreSQL.
Source code: services/internal/greeter/service.goEntry point: services/cmd/greeter/main.go

Technology Stack

  • Language: Go
  • Framework: Connect-Go (gRPC-compatible)
  • Database: PostgreSQL via pgx/v5
  • Protocol: Protocol Buffers
  • Observability: OpenTelemetry
  • HTTP Version: HTTP/2 (h2c)

Configuration

Environment Variables

PORT
string
default:"8080"
HTTP server port for the greeter service
CALLER_BASE_URL
string
default:"http://caller-service.microservices:8081"
Base URL for the Caller serviceDocker Compose uses: http://caller:8081
EXTERNAL_API_URL
string
default:"https://httpbin.org/get"
External API URL that the Caller service will fetch
DATABASE_URL
string
required
PostgreSQL connection stringExample: postgresql://devuser:devpass@postgres:5432/greeter_db
OTEL_EXPORTER_OTLP_ENDPOINT
string
OpenTelemetry collector endpoint
OTEL_SERVICE_NAME
string
default:"greeter-service"
Service name for distributed tracing

Docker Compose Configuration

greeter:
  build:
    context: .
    dockerfile: deploy/docker/greeter/Dockerfile
  environment:
    PORT: "8080"
    CALLER_BASE_URL: "http://caller:8081"
    EXTERNAL_API_URL: "https://httpbin.org/get"
    DATABASE_URL: "postgresql://devuser:devpass@postgres:5432/greeter_db"
    OTEL_SERVICE_NAME: "greeter-service"
  ports:
    - "8080:8080"
  depends_on:
    - caller
    - postgres

API Reference

Protocol Buffer Definition

The service is defined in proto/greeter/v1/greeter.proto:
syntax = "proto3";

package greeter.v1;

service GreeterService {
  rpc Greet(GreetRequest) returns (GreetResponse) {}
}

message GreetRequest {
  string name = 1;
}

message GreetResponse {
  string message = 1;
  int32 external_status = 2;
  int32 external_body_length = 3;
}

Greet RPC

Generates a personalized greeting and fetches external API data via the Caller service.
name
string
default:"World"
Name to include in the greeting message
message
string
The greeting messageFormat: "Hello {name} from greeter-service!"
external_status
int32
HTTP status code from the external API call made by Caller service
external_body_length
int32
Size in bytes of the external API response body

Example Request

# Using grpcurl
grpcurl -plaintext -d '{"name": "Alice"}' \
  localhost:8080 \
  greeter.v1.GreeterService/Greet

Example Response

{
  "message": "Hello Alice from greeter-service!",
  "external_status": 200,
  "external_body_length": 384
}

Implementation Details

Service Structure

From services/internal/greeter/service.go:19-33:
type Service struct {
    callerClient callerv1connect.CallerServiceClient
    externalURL  string
    timeout      time.Duration
    pool         *pgxpool.Pool
}

func NewService(
    callerClient callerv1connect.CallerServiceClient, 
    externalURL string, 
    timeout time.Duration, 
    pool *pgxpool.Pool,
) *Service {
    return &Service{
        callerClient: callerClient,
        externalURL:  externalURL,
        timeout:      timeout,
        pool:         pool,
    }
}

Request Flow

  1. Receive Request: Accept name parameter (defaults to “World”)
  2. Call External API: Invoke Caller service with timeout
  3. Error Handling: Map Connect errors and timeouts appropriately
  4. Generate Response: Format greeting message
  5. Database Write: Synchronously insert greeting log
  6. Return Response: Send complete response to client
From services/internal/greeter/service.go:35-72:
func (s *Service) Greet(
    ctx context.Context, 
    req *connect.Request[greeterv1.GreetRequest],
) (*connect.Response[greeterv1.GreetResponse], error) {
    name := req.Msg.GetName()
    if name == "" {
        name = "World"
    }

    rpcCtx, cancel := context.WithTimeout(ctx, s.timeout)
    defer cancel()

    callerResp, err := s.callerClient.CallExternal(
        rpcCtx, 
        connect.NewRequest(&callerv1.CallExternalRequest{Url: s.externalURL}),
    )
    if err != nil {
        var connectErr *connect.Error
        if errors.As(err, &connectErr) {
            return nil, err
        }
        if rpcCtx.Err() != nil {
            return nil, connect.NewError(connect.CodeDeadlineExceeded, rpcCtx.Err())
        }
        return nil, connect.NewError(connect.CodeUnavailable, 
            fmt.Errorf("caller call failed: %w", err))
    }

    msg := fmt.Sprintf("Hello %s from greeter-service!", name)
    resp := connect.NewResponse(&greeterv1.GreetResponse{
        Message:            msg,
        ExternalStatus:     callerResp.Msg.GetStatusCode(),
        ExternalBodyLength: callerResp.Msg.GetBodyLength(),
    })

    // Synchronous DB write pattern
    if s.pool != nil {
        _, dbErr := s.pool.Exec(ctx, 
            "INSERT INTO greetings (name, message, external_status) VALUES ($1, $2, $3)", 
            name, msg, callerResp.Msg.GetStatusCode())
        if dbErr != nil {
            slog.Error("failed to insert greeting", "error", dbErr)
        }
    }

    return resp, nil
}

Timeout Configuration

  • RPC Timeout: 2 seconds for Caller service calls
  • HTTP Client Timeout: 3 seconds for Connect client
  • Server Read Timeout: 5 seconds
  • Server Write Timeout: 30 seconds

Database Schema

The service uses a greetings table to log all greeting requests:
CREATE TABLE greetings (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL,
    message TEXT NOT NULL,
    external_status INT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

Database Pattern

The Greeter service uses a synchronous write pattern:
  • Database writes complete before sending the response
  • Guarantees data persistence
  • Adds latency to response time (~1-5ms typically)
  • Failures are logged but don’t affect the response
Compare this with the Caller service’s asynchronous pattern where writes happen after the response.

Service Dependencies

Upstream Dependencies

  • Caller Service: Required for external API calls
  • PostgreSQL: Optional, service runs without DB but won’t persist logs

Downstream Consumers

  • Frontend: Via Traefik gateway
  • Direct gRPC Clients: Any Connect-compatible client

Error Handling

The service handles multiple error scenarios:

Timeout Errors

if rpcCtx.Err() != nil {
    return nil, connect.NewError(connect.CodeDeadlineExceeded, rpcCtx.Err())
}
Returns: Code.DEADLINE_EXCEEDED

Caller Service Unavailable

return nil, connect.NewError(connect.CodeUnavailable, 
    fmt.Errorf("caller call failed: %w", err))
Returns: Code.UNAVAILABLE

Database Errors

Database errors are logged but don’t fail the request:
if dbErr != nil {
    slog.Error("failed to insert greeting", "error", dbErr)
}

Observability

Structured Logging

The service uses Go’s slog package with context-aware logging:
logger.InfoContext(ctx, "starting greeter service", "port", port)

Distributed Tracing

OpenTelemetry instrumentation captures:
  • RPC call spans
  • Service-to-service calls
  • Database operations
  • Error tracking
Traces are exported to the configured OTLP endpoint.

Health Checks

Health endpoint at /healthz performs database ping:
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
    if dbPool != nil {
        if err := dbPool.Ping(r.Context()); err != nil {
            w.WriteHeader(http.StatusServiceUnavailable)
            _, _ = w.Write([]byte("db unhealthy\n"))
            return
        }
    }
    w.WriteHeader(http.StatusOK)
    _, _ = w.Write([]byte("ok\n"))
})

Testing

Test the service using grpcurl:
# Basic greeting
grpcurl -plaintext -d '{"name": "Alice"}' \
  localhost:8080 \
  greeter.v1.GreeterService/Greet

# Empty name (uses default "World")
grpcurl -plaintext -d '{}' \
  localhost:8080 \
  greeter.v1.GreeterService/Greet

# Health check
curl http://localhost:8080/healthz

Traffic Routing

Traefik routes requests based on the gRPC service path:
labels:
  - "traefik.enable=true"
  - "traefik.http.routers.greeter.rule=PathPrefix(`/greeter.v1.GreeterService`)"
  - "traefik.http.routers.greeter.middlewares=cors@file,auth@file,rate-limit@file,retry@file"
  - "traefik.http.services.greeter.loadbalancer.server.scheme=h2c"
Middleware applied:
  • CORS: Cross-origin resource sharing
  • Auth: JWT validation via auth-service
  • Rate Limiting: Prevents abuse
  • Retry: Automatic retry on failures

Performance Characteristics

  • Latency: ~5-20ms (depends on Caller service + external API)
  • Throughput: Limited by database connection pool (default: 10 connections)
  • Database Write: Synchronous, adds ~1-5ms
  • Timeout Budget: 2 seconds total

Common Issues

Caller Service Unavailable

Error: Code.UNAVAILABLE: caller call failed Solution: Ensure Caller service is running and accessible at CALLER_BASE_URL

Database Connection Failed

Log: database unavailable, running without DB Impact: Service runs but doesn’t persist greeting logs Solution: Check DATABASE_URL and ensure PostgreSQL is accessible

Timeout Errors

Error: Code.DEADLINE_EXCEEDED Cause: External API call taking >2 seconds Solution: Increase timeout or investigate external API latency

Caller Service

Makes the external API calls for Greeter

Services Overview

Architecture and communication patterns

Build docs developers (and LLMs) love