Skip to content

pay-theory/dynamorm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

57 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

DynamORM: Type-Safe DynamoDB ORM for Go

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.

Go Version License Go Report Card

Why DynamORM?

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

Features

  • 🚀 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

Quick Start

// 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
// }

Installation

For Lambda Functions (Recommended)

# 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{})

For Standard Applications

# 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"})

For Local Development

# 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
})

Core Concepts

Model Definition - CRITICAL for AI Assistants

Models are how DynamORM understands your DynamoDB table structure. AI assistants MUST use exact canonical patterns to prevent struct hallucinations.

⚠️ AI Warning: DO NOT invent struct patterns. Use exact examples from Struct Definition Guide

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.

Query Builder Pattern

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
// }

Common Patterns

Pattern: DynamoDB Streams Processing

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
}

Pattern: Lambda Handler

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
// }

Pattern: Testable Service

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
// }

Pattern: Transaction Operations

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!

Performance Benchmarks

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

Troubleshooting

Error: "ValidationException: One or more parameter values were invalid"

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

Error: "ResourceNotFoundException: Requested resource not found"

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)

Error: "Query cost is too high" or slow performance

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

Error: Cold start timeouts in Lambda

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
// }

Migration Guide

From AWS SDK v1

// 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

From GORM

// 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

Best Practices

  1. ALWAYS use struct tags for DynamoDB schema mapping
  2. ALWAYS initialize DynamORM in init() for Lambda functions
  3. ALWAYS use interfaces (core.DB) for testable code
  4. NEVER initialize database connections in request handlers
  5. NEVER scan tables without indexes - use Query with proper keys
  6. PREFER transactions for multi-item consistency requirements
  7. PREFER batch operations for multiple items of same type

API Reference

Model(entity interface{}) Query

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

Where(field, operator, value) Query

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)

Transaction(fn func(*Tx) error) error

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()
})

About This Codebase

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.

About

No description, website, or topics provided.

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •