DynamORM provides a type-safe, optimized way to interact with Amazon DynamoDB in Go applications. It offers significantly faster cold starts than raw AWS SDK and reduces boilerplate code.
Use DynamORM when you need:
- ✅ Type-safe DynamoDB operations - Compile-time error prevention
- ✅ Lambda-optimized performance - Reduced cold starts and memory usage
- ✅ Less boilerplate code - Intuitive API vs verbose AWS SDK
- ✅ Built-in testing support - Interfaces and mocks for testable code
- ✅ Production-ready patterns - Transactions, consistency, error handling
❌ Don't use DynamORM for:
- Non-DynamoDB databases
- Applications requiring SQL-style joins
- Direct AWS SDK control requirements
- 🚀 Type-Safe: Full compile-time type safety with Go generics
- ⚡ Lambda Optimized: Sub-15ms cold starts with connection reuse
- 🔄 Auto-Generated Keys: Composite key generation from struct tags
- 🔍 Smart Querying: Intuitive query builder with index management
- 📦 Batch Operations: Efficient batch read/write operations
- 🔐 Transaction Support: ACID transactions across multiple items
- 🎯 Zero Configuration: Works out of the box with sensible defaults
- 🧪 Testable: Built-in mocks and testing utilities
- 💰 Cost Tracking: Integrated consumed capacity monitoring
- 🏗️ Schema Management: Automatic table creation and migrations
- 🌊 Stream Processing: Native DynamoDB Streams support with UnmarshalItem/UnmarshalItems
// This demonstrates the DynamORM pattern for DynamoDB operations in Go
// It provides type safety, error handling, and Lambda optimization
package main
import (
"log"
"github.com/pay-theory/dynamorm"
"github.com/pay-theory/dynamorm/pkg/session"
)
// CORRECT: Always define models with struct tags
type User struct {
ID string `dynamorm:"pk"` // Partition key
Email string `dynamorm:"sk"` // Sort key
Name string
CreatedAt int64 `dynamorm:"created_at"`
}
func main() {
// CORRECT: Initialize with proper configuration
db, err := dynamorm.New(session.Config{
Region: "us-east-1",
// For Lambda: use NewLambdaOptimized() or LambdaInit()
// For local dev: Endpoint: "http://localhost:8000"
})
if err != nil {
log.Fatal("Failed to initialize DynamORM:", err)
}
// CORRECT: Type-safe operations with error handling
user := &User{
ID: "user123",
Email: "john@example.com",
Name: "John Doe",
}
// Create operation - automatic validation and marshaling
if err := db.Model(user).Create(); err != nil {
log.Printf("Create failed: %v", err)
}
// Query operation - type-safe results
var users []User
err = db.Model(&User{}).
Where("ID", "=", "user123").
All(&users)
if err != nil {
log.Printf("Query failed: %v", err)
}
}
// INCORRECT: Don't use raw DynamoDB SDK like this:
//
// import "github.com/aws/aws-sdk-go/service/dynamodb"
//
// func badExample() {
// // This pattern lacks type safety and requires verbose marshaling
// input := &dynamodb.PutItemInput{
// TableName: aws.String("users"),
// Item: map[string]*dynamodb.AttributeValue{
// "id": {S: aws.String("user123")},
// // ... verbose attribute mapping
// },
// }
// // Prone to runtime errors and marshaling mistakes
// }
# This method is preferred for AWS Lambda deployments
go get github.com/pay-theory/dynamorm
# Lambda-optimized initialization:
db, err := dynamorm.NewLambdaOptimized()
# or
db, err := dynamorm.LambdaInit(&User{})
# Use this method for long-running applications
go get github.com/pay-theory/dynamorm
# Standard initialization:
db, err := dynamorm.New(session.Config{Region: "us-east-1"})
# Install with DynamoDB Local support
go get github.com/pay-theory/dynamorm
# Local development configuration:
db, err := dynamorm.New(session.Config{
Region: "us-east-1",
Endpoint: "http://localhost:8000", // DynamoDB Local
})
Models are how DynamORM understands your DynamoDB table structure. AI assistants MUST use exact canonical patterns to prevent struct hallucinations.
Example - Simple Entity (Most Common):
// CANONICAL PATTERN: Use this EXACT format for basic entities
type User struct {
ID string `dynamorm:"pk" json:"id"` // REQUIRED: Partition key
Email string `dynamorm:"sk" json:"email"` // OPTIONAL: Sort key
Name string `json:"name"` // Standard field
Active bool `json:"active"` // Boolean field
CreatedAt time.Time `json:"created_at"` // Timestamp field
}
// CANONICAL PATTERN: Entity with GSI for alternate queries
type Payment struct {
ID string `dynamorm:"pk" json:"id"` // Primary partition key
Timestamp string `dynamorm:"sk" json:"timestamp"` // Primary sort key
CustomerID string `dynamorm:"index:customer-index,pk" json:"customer_id"` // GSI partition key
Status string `dynamorm:"index:customer-index,sk" json:"status"` // GSI sort key
Amount int64 `json:"amount"` // Standard field
CreatedAt time.Time `json:"created_at"` // Timestamp field
}
// ❌ FORBIDDEN: These patterns DO NOT EXIST
// type BadPayment struct {
// ID string `dynamorm:"partition_key"` // WRONG: Use "pk"
// Amount int64 `dynamorm:"attribute"` // WRONG: No "attribute" tag
// Status string `dynamorm:"gsi:status"` // WRONG: Use "index:name,pk"
// }
📋 Required for Every Struct:
- At least one
dynamorm:"pk"
field (partition key) - Proper
json:
tags for all fields - Only supported Go types (string, int, bool, time.Time, []string, map[string]string)
- Follow naming: PascalCase in Go, snake_case in JSON
🔗 Complete Guidance: See Struct Definition Guide for all canonical patterns.
DynamORM uses a fluent query builder that automatically selects optimal indexes and generates efficient DynamoDB queries.
Example:
// CORRECT: Chainable query building with automatic optimization
var payments []Payment
err := db.Model(&Payment{}).
Index("amount-index"). // Explicit index selection
Where("Amount", ">", 1000). // Type-safe conditions
OrderBy("Timestamp", "DESC"). // Automatic sort key handling
Limit(10). // Result limiting
ConsistentRead(). // Strong consistency when needed
All(&payments) // Execute and unmarshal
// INCORRECT: Don't build queries manually
// This will cause performance issues and errors:
// input := &dynamodb.QueryInput{
// TableName: aws.String("payments"),
// IndexName: aws.String("amount-index"),
// // ... complex expression building prone to errors
// }
When to use: Processing DynamoDB stream events in Lambda
// Use DynamORM's UnmarshalItem for processing DynamoDB stream records
// This ensures consistency with your DynamORM models
import (
"github.com/aws/aws-lambda-go/events"
"github.com/pay-theory/dynamorm"
)
func handleDynamoDBStream(ctx context.Context, event events.DynamoDBEvent) error {
for _, record := range event.Records {
switch record.EventName {
case "INSERT", "MODIFY":
var order Order
// Use DynamORM's UnmarshalItem instead of AWS SDK
if err := dynamorm.UnmarshalItem(record.Change.NewImage, &order); err != nil {
return fmt.Errorf("failed to unmarshal: %w", err)
}
// Process the order...
log.Printf("Order %s status: %s", order.OrderID, order.Status)
case "REMOVE":
var order Order
if err := dynamorm.UnmarshalItem(record.Change.OldImage, &order); err != nil {
return fmt.Errorf("failed to unmarshal: %w", err)
}
log.Printf("Order %s was removed", order.OrderID)
}
}
return nil
}
// For batch processing of stream records
func processBatchRecords(records []events.DynamoDBEventRecord) error {
// Extract all new images
var items []map[string]types.AttributeValue
for _, record := range records {
if record.Change.NewImage != nil {
items = append(items, record.Change.NewImage)
}
}
// Unmarshal all at once
var orders []Order
if err := dynamorm.UnmarshalItems(items, &orders); err != nil {
return fmt.Errorf("failed to unmarshal batch: %w", err)
}
// Process orders...
return nil
}
When to use: Building AWS Lambda functions with DynamoDB Why: Optimizes cold starts and provides automatic resource management
// CORRECT: Lambda-optimized pattern
package main
import (
"context"
"github.com/aws/aws-lambda-go/lambda"
"github.com/pay-theory/dynamorm"
)
var db *dynamorm.LambdaDB
func init() {
// Initialize once, reuse across invocations
// This reduces cold start time significantly
var err error
db, err = dynamorm.NewLambdaOptimized()
if err != nil {
panic(err)
}
}
func handler(ctx context.Context, event PaymentEvent) error {
// Business logic using pre-initialized connection
payment := &Payment{
ID: event.PaymentID,
Amount: event.Amount,
}
return db.Model(payment).Create()
}
func main() {
lambda.Start(handler)
}
// INCORRECT: Don't initialize in handler
// This causes slow cold starts:
// func handler(ctx context.Context, event PaymentEvent) error {
// db := dynamorm.New(...) // Creates new connection every time
// // ... rest of handler
// }
When to use: Building testable business logic Why: Enables unit testing without DynamoDB dependency
// CORRECT: Interface-based dependency injection
import "github.com/pay-theory/dynamorm/pkg/core"
type PaymentService struct {
db core.DB // Interface allows mocking
}
func NewPaymentService(db core.DB) *PaymentService {
return &PaymentService{db: db}
}
func (s *PaymentService) CreatePayment(payment *Payment) error {
// Business logic that can be tested
return s.db.Model(payment).Create()
}
// Test example:
import (
"testing"
"github.com/pay-theory/dynamorm/pkg/mocks"
"github.com/stretchr/testify/mock"
)
func TestPaymentService(t *testing.T) {
// CORRECT: Use provided mocks for testing
mockDB := new(mocks.MockDB)
mockQuery := new(mocks.MockQuery)
mockDB.On("Model", mock.Anything).Return(mockQuery)
mockQuery.On("Create").Return(nil)
service := NewPaymentService(mockDB)
err := service.CreatePayment(&Payment{})
assert.NoError(t, err)
mockDB.AssertExpectations(t)
}
// INCORRECT: Don't use concrete types
// type BadService struct {
// db *dynamorm.DB // Cannot be mocked easily
// }
When to use: Multiple operations that must succeed or fail together Why: Ensures data consistency and ACID compliance
// CORRECT: Transaction pattern for consistent operations
err := db.Transaction(func(tx *dynamorm.Tx) error {
// All operations must succeed or entire transaction rolls back
// Debit account
account.Balance -= payment.Amount
if err := tx.Model(account).Update(); err != nil {
return err // Automatic rollback
}
// Create payment record
payment.Status = "completed"
if err := tx.Model(payment).Create(); err != nil {
return err // Automatic rollback
}
// Create audit log
audit := &AuditLog{
Action: "payment_processed",
PaymentID: payment.ID,
Amount: payment.Amount,
}
return tx.Model(audit).Create()
})
if err != nil {
log.Printf("Transaction failed: %v", err)
// All operations were rolled back automatically
}
// INCORRECT: Don't perform separate operations
// This can leave data in inconsistent state:
// db.Model(account).Update() // Might succeed
// db.Model(payment).Create() // Might fail - inconsistent state!
Based on our benchmarks, DynamORM provides significant performance improvements when properly configured:
Metric | DynamORM | AWS SDK | Improvement |
---|---|---|---|
Lambda Cold Start | 11ms | 127ms | 91% faster |
Memory Usage | 18MB | 42MB | 57% less |
Single Item Lookup | 0.52ms | 0.51ms | Near parity |
Batch Operations | 45ms | 78ms | 42% faster |
Code Lines (CRUD) | ~20 | ~100 | ~80% less |
Note: Performance varies based on configuration, table design, and workload. Lambda optimizations require using NewLambdaOptimized()
or LambdaInit()
.
These improvements come from:
- Connection pooling and reuse
- Optimized marshaling/unmarshaling
- Intelligent query planning
- Reduced memory allocations
Cause: This happens when struct tags don't match table schema Solution: Verify your struct tags match your DynamoDB table definition
// Check your model definition:
type User struct {
ID string `dynamorm:"pk"` // Must match table's partition key
Email string `dynamorm:"sk"` // Must match table's sort key (if exists)
}
// Verify table schema matches:
// aws dynamodb describe-table --table-name users
Cause: Table doesn't exist or wrong table name Solution: Create table or verify configuration
// Option 1: Create table from model (development only)
err := db.CreateTable(&User{})
// Option 2: Verify table name in AWS console matches model
// Table name is derived from struct name (User -> users)
Cause: Query not using optimal index or scanning instead of querying Solution: Use explicit index selection and proper key conditions
// CORRECT: Use specific index for efficient queries
err := db.Model(&Payment{}).
Index("status-index"). // Explicit index
Where("Status", "=", "pending"). // Partition key condition
Where("CreatedAt", ">", yesterday). // Sort key condition (optional)
All(&payments)
// INCORRECT: Don't scan entire table
// err := db.Model(&Payment{}).Where("Amount", ">", 100).All(&payments)
// This scans entire table instead of using index
Cause: Not using Lambda optimizations or initializing in handler Solution: Use Lambda optimizations and initialize in init()
// CORRECT: Initialize once in init() with optimizations
var db *dynamorm.LambdaDB
func init() {
var err error
db, err = dynamorm.NewLambdaOptimized()
if err != nil {
panic(err)
}
}
// INCORRECT: Don't initialize in handler
// func handler() {
// db := dynamorm.New(...) // Slow cold start
// }
// Old pattern with AWS SDK v1 (replace this):
import "github.com/aws/aws-sdk-go/service/dynamodb"
func oldCreateUser(svc *dynamodb.DynamoDB, user User) error {
input := &dynamodb.PutItemInput{
TableName: aws.String("users"),
Item: map[string]*dynamodb.AttributeValue{
"id": {S: aws.String(user.ID)},
"email": {S: aws.String(user.Email)},
"name": {S: aws.String(user.Name)},
},
}
_, err := svc.PutItem(input)
return err
}
// New pattern with DynamORM (use this instead):
func newCreateUser(db *dynamorm.DB, user *User) error {
return db.Model(user).Create()
}
// Benefits: 80% less code, type safety, automatic marshaling
// Old pattern with GORM (SQL-based):
func oldPattern() {
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
var users []User
db.Where("age > ?", 18).Find(&users)
}
// New pattern with DynamORM (NoSQL-optimized):
func newPattern() {
db := dynamorm.New(session.Config{Region: "us-east-1"})
var users []User
db.Model(&User{}).
Index("age-index"). // Explicit index for NoSQL
Where("Age", ">", 18).
All(&users)
}
// Benefits: NoSQL optimization, better performance, cloud-native
- ALWAYS use struct tags for DynamoDB schema mapping
- ALWAYS initialize DynamORM in init() for Lambda functions
- ALWAYS use interfaces (core.DB) for testable code
- NEVER initialize database connections in request handlers
- NEVER scan tables without indexes - use Query with proper keys
- PREFER transactions for multi-item consistency requirements
- PREFER batch operations for multiple items of same type
Purpose: Creates a type-safe query builder for the given entity type When to use: Starting any DynamoDB operation (Create, Read, Update, Delete) When NOT to use: Don't call Model() multiple times in the same operation chain
// Example: Basic model usage
db.Model(&User{}) // Query builder for User table
db.Model(user) // For operations on existing instance
db.Model(&users) // For operations returning multiple results
// This returns a Query interface for chaining operations
Purpose: Adds type-safe condition to query (translates to DynamoDB KeyConditionExpression or FilterExpression) When to use: Filtering results by attribute values When NOT to use: Don't use Where() without proper index for large tables
// Example: Query with conditions
db.Model(&Payment{}).
Where("Status", "=", "pending"). // Key condition (indexed field)
Where("Amount", ">", 1000). // Filter expression
All(&payments)
Purpose: Executes multiple operations atomically within a DynamoDB transaction When to use: Operations that must all succeed or all fail together When NOT to use: Single operations or operations across different AWS accounts
// Example: Transfer money between accounts
err := db.Transaction(func(tx *dynamorm.Tx) error {
// All operations execute atomically
if err := tx.Model(fromAccount).Update(); err != nil {
return err // Rolls back entire transaction
}
return tx.Model(toAccount).Update()
})
This entire codebase was written 100% by AI code generation, guided by the development team at Pay Theory. The framework represents a collaboration between human architectural vision and AI implementation capabilities, demonstrating the potential of AI-assisted software development for creating production-ready systems.