A cloud-agnostic serverless API for managing e-commerce orders with multi-tenant support and PostgreSQL storage, written in Go.
This project uses a separated handler pattern that isolates cloud provider-specific code from core business logic, making it easy to migrate between AWS, Azure, GCP, or OCI.
ecommerce-order-api/
├── core/
│ └── order_service.go # Cloud-agnostic business logic
├── handlers/
│ ├── aws/main.go # AWS Lambda handler
│ ├── azure/main.go # Azure Functions handler
│ └── gcp/main.go # GCP Cloud Functions handler
├── database/
│ └── schema.sql # PostgreSQL schema
└── aws/
└── template.yaml # SAM template
- âś… Multi-tenant architecture with tenant isolation
- âś… Authentication integration with Cognito/Azure AD/Firebase
- âś… PostgreSQL database for reliable data storage
- âś… RESTful API for order management
- âś… Cloud-agnostic core logic for easy migration
- âś… Type-safe Go implementation
- âś… Efficient connection pooling
- Go 1.21 or higher
- AWS CLI and SAM CLI (for AWS deployment)
- PostgreSQL database
- Make (optional, for using Makefile)
go mod download# Connect to your PostgreSQL instance
psql -h your-db-host -U your-user -d ecommerce
# Run schema
\i database/schema.sql# Using Make
make build-aws
# Or manually
cd handlers/aws
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -tags lambda.norpc -o bootstrap main.go# Set environment variables
export DB_HOST=your-db.rds.amazonaws.com
export DB_USER=your_user
export DB_PASSWORD=your_pass
export COGNITO_USER_POOL_ARN=arn:aws:cognito-idp:...
# Deploy using SAM
make deploy-aws
# Or manually
cd aws
sam build
sam deploy --guided \
--parameter-overrides \
DBHost=$DB_HOST \
DBName=ecommerce \
DBUser=$DB_USER \
DBPassword=$DB_PASSWORD \
CognitoUserPoolArn=$COGNITO_USER_POOL_ARNPOST /orders
Authorization: Bearer <cognito-token>
Content-Type: application/json
{
"customer_name": "John Doe",
"customer_email": "john@example.com",
"customer_phone": "+1234567890",
"shipping_address": "123 Main St",
"billing_address": "123 Main St",
"total_amount": 99.99,
"currency": "USD",
"items": [
{
"product_name": "Widget",
"product_sku": "WDG-001",
"quantity": 2,
"unit_price": 49.99
}
]
}Response (201 Created):
{
"message": "Order created successfully",
"order": {
"order_id": "uuid-here",
"tenant_id": "tenant-123",
"customer_name": "John Doe",
"total_amount": 99.99,
"status": "pending",
"items": [...]
}
}GET /orders/{order_id}
Authorization: Bearer <cognito-token>GET /orders?limit=50&offset=0&my_orders=true
Authorization: Bearer <cognito-token>The API extracts authentication claims from the cloud provider's authentication service:
type AuthClaims struct {
TenantID string // From custom:tenant_id
UserID string // From sub
Email string // From email
}Configure Cognito User Pool with custom attribute custom:tenant_id:
# Create user with tenant_id
aws cognito-idp admin-update-user-attributes \
--user-pool-id <pool-id> \
--username user@example.com \
--user-attributes Name=custom:tenant_id,Value=tenant-123-
core/order_service.go - Cloud-agnostic business logic
NewOrderService()- Initialize service with DB configCreateOrder()- Create order with validationGetOrder()- Retrieve order with tenant isolationListOrders()- List orders with pagination
-
handlers/aws/main.go - AWS Lambda specific
- Extracts Cognito claims
- Formats API Gateway responses
- Routes HTTP methods
# Run all tests
make test
# Or
go test -v ./...# Start local API
make local-aws
# Or
cd aws && sam local start-api
# Test endpoints
curl -X POST http://localhost:3000/orders \
-H "Content-Type: application/json" \
-d @../test_order.json# Format code
make fmt
# Or
go fmt ./...- Build the binary:
make build-aws- Deploy with SAM:
cd aws
sam build
sam deploy --guided- Get API endpoint:
aws cloudformation describe-stacks \
--stack-name ecommerce-order-api \
--query 'Stacks[0].Outputs[?OutputKey==`OrderApiUrl`].OutputValue' \
--output text- Build:
make build-azure- Deploy:
# Create function app
az functionapp create \
--resource-group your-rg \
--consumption-plan-location eastus \
--runtime custom \
--functions-version 4 \
--name your-function-app
# Configure settings
az functionapp config appsettings set \
--name your-function-app \
--resource-group your-rg \
--settings \
DB_HOST=$DB_HOST \
DB_NAME=ecommerce \
DB_USER=$DB_USER \
DB_PASSWORD=$DB_PASSWORD
# Deploy (create host.json and function.json first)
func azure functionapp publish your-function-app- Build:
make build-gcp- Deploy:
gcloud functions deploy order-api \
--gen2 \
--runtime go121 \
--entry-point OrderAPI \
--trigger-http \
--allow-unauthenticated \
--set-env-vars \
DB_HOST=$DB_HOST,\
DB_NAME=ecommerce,\
DB_USER=$DB_USER,\
DB_PASSWORD=$DB_PASSWORD- Build and push container:
gcloud builds submit --tag gcr.io/YOUR_PROJECT_ID/order-api -f gcp/cloudrun/Dockerfile- Deploy:
gcloud run deploy order-api \
--image gcr.io/YOUR_PROJECT_ID/order-api \
--region YOUR_REGION \
--allow-unauthenticated \
--set-env-vars \
DB_HOST=$DB_HOST,\
DB_NAME=ecommerce,\
DB_USER=$DB_USER,\
DB_PASSWORD=$DB_PASSWORDAll tables include tenant_id for data isolation:
CREATE TABLE orders (
order_id UUID PRIMARY KEY,
tenant_id VARCHAR(255) NOT NULL,
created_by VARCHAR(255) NOT NULL,
...
);
-- Composite index for tenant queries
CREATE INDEX idx_orders_tenant_id ON orders(tenant_id);
CREATE INDEX idx_orders_tenant_created_by ON orders(tenant_id, created_by);The OrderService manages connection pooling:
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)- Lambda Cold Start: Using ARM64 architecture reduces cold start time
- Connection Reuse: DB connection is reused across Lambda invocations
- Indexed Queries: All queries use indexed columns
- Prepared Statements: PostgreSQL query optimization
To migrate from AWS to Azure or GCP:
- No changes needed to
core/order_service.go - Build the target handler:
make build-azure # or make build-gcp - Deploy using the target platform's CLI
- Update authentication configuration
The core business logic remains identical!
-
Secrets Management:
- AWS: Use Secrets Manager
- Azure: Use Key Vault
- GCP: Use Secret Manager
-
Database Security:
- Use SSL/TLS connections (
sslmode=require) - Store credentials in secrets manager
- Enable VPC/Private networking
- Use SSL/TLS connections (
-
Multi-tenancy:
- All queries filtered by
tenant_id - Claims extracted from verified tokens only
- Never accept tenant_id from request body
- All queries filtered by
# View logs
aws logs tail /aws/lambda/EcommerceOrderFunction --follow
# View metrics
aws cloudwatch get-metric-statistics \
--namespace AWS/Lambda \
--metric-name Duration \
--dimensions Name=FunctionName,Value=EcommerceOrderFunction \
--start-time 2024-01-01T00:00:00Z \
--end-time 2024-01-02T00:00:00Z \
--period 3600 \
--statistics Average-
"Failed to connect to database"
- Check security groups allow Lambda to access RDS
- Verify database credentials
- Ensure database is accessible
-
"Missing tenant_id in claims"
- Verify Cognito custom attribute is configured
- Check user has tenant_id attribute set
- Decode JWT token to verify claims
-
Build errors
- Ensure Go 1.21+ is installed
- Run
go mod download - Check GOOS and GOARCH are correct
# Decode JWT token
echo $TOKEN | cut -d'.' -f2 | base64 -d | jq
# Test database connection
psql -h $DB_HOST -U $DB_USER -d $DB_NAME -c "SELECT 1"
# Check Lambda logs
aws logs tail /aws/lambda/EcommerceOrderFunction --follow- Fork the repository
- Create a feature branch
- Make your changes
- Run tests:
make test - Format code:
make fmt - Submit a pull request
MIT