Skip to content

Repository Pattern

Repositories provide the primary interface for interacting with DynamoDB tables. They offer CRUD operations, query building, and automatic table name resolution.

Defining Repositories

Repositories embed dynorm.Repository[T] where T is your entity type:

// Table name auto-derived: User → users
type UserRepository struct {
    dynorm.Repository[User]
}

var UserRepo = &UserRepository{}

// With explicit table name
type StaffRepository struct {
    dynorm.Repository[User] `table:"staff"`
}

var StaffRepo = &StaffRepository{}

Table Name Resolution

Table names are automatically resolved:

  1. Type name → plural snake_case: Userusers, CreditCardcredit_cards
  2. Table tag override: table:"custom_name" overrides auto-derivation
  3. Prefix from environment: DYNORM_TABLE_PREFIX prepended to all names
// With DYNORM_TABLE_PREFIX=prod-
// User entity → "prod-users"
// CreditCard entity → "prod-credit_cards"

CRUD Operations

Find

Retrieves an entity by its primary key (ULID ID):

user, err := UserRepo.Find("01HQ3K4N5M6P7Q8R9S0T1U2V3W")
if err != nil {
    return err
}
if user == nil {
    // Not found
}

Save

Creates or updates an entity. For new entities, a ULID is auto-generated:

// Create new user
user := &User{
    Email:     "john@example.com",
    FirstName: "John",
    LastName:  "Doe",
}
err := UserRepo.Save(user)
// user.ID is now set: "01HQ3K..."
// user.CreatedAt derived from ULID timestamp
// user.UpdatedAt set to current time

// Update existing user
user.Status = "verified"
err = UserRepo.Save(user)
// user.UpdatedAt updated

Delete

Removes an entity from the database:

// Delete by entity
err := UserRepo.Delete(user)

// Delete by ID
err := UserRepo.DeleteByID("01HQ3K4N5M6P7Q8R9S0T1U2V3W")

All

Returns all entities (performs a table scan):

users, err := UserRepo.All()

Performance

All() performs a full table scan. Use queries with conditions for large tables.

Count

Returns the total count of entities:

count, err := UserRepo.Count()

Query Building

Repositories provide fluent query building methods:

// Start a query with Where
users, err := UserRepo.
    Where("Status", "=", "active").
    GetAll()

// Start with Select
users, err := UserRepo.
    Select("ID", "Email", "FirstName").
    Where("Status", "=", "active").
    GetAll()

// Start with OrderBy
users, err := UserRepo.
    OrderBy("CreatedAt", "desc").
    Limit(10).
    GetAll()

// Start with Limit
users, err := UserRepo.
    Limit(100).
    GetAll()

See Query Builder for complete query documentation.

Soft Deletes

Repositories support soft delete operations for entities with soft_deletes:"true":

type Post struct {
    dynorm.Entity `soft_deletes:"true"`
    Title   string
    Content string
}

type PostRepository struct {
    dynorm.Repository[Post]
}

var PostRepo = &PostRepository{}

Soft Delete

Marks an entity as deleted without removing it:

err := PostRepo.SoftDelete(post)
// post.DeletedAt is now set
// post still exists in database

Restore

Removes the soft delete marker:

err := PostRepo.Restore(post)
// post.DeletedAt is now nil

Architecture

graph TB
    subgraph "Repository Pattern"
        Repo[Repository<T>]
        Entity[Entity]
        Query[Query Builder]
        Schema[Schema Parser]
    end

    subgraph "Operations"
        Find[Find by ID]
        Save[Save]
        Delete[Delete]
        All[All]
        Count[Count]
    end

    subgraph "AWS"
        DDB[(DynamoDB)]
    end

    Repo --> Entity
    Repo --> Query
    Repo --> Schema

    Repo --> Find
    Repo --> Save
    Repo --> Delete
    Repo --> All
    Repo --> Count

    Find --> DDB
    Save --> DDB
    Delete --> DDB
    All --> DDB
    Count --> DDB

Lazy Initialization

Repositories initialize lazily on first use:

  1. Schema is parsed from struct tags
  2. Table name is resolved (type name + prefix + tag override)
  3. DynamoDB client is initialized (via sync.Once)
// No initialization happens here
var UserRepo = &UserRepository{}

// First use triggers initialization
user, err := UserRepo.Find("01HQ3K...")

Batch Operations

Repositories support batch operations for efficiency:

// Batch get
users, err := UserRepo.BatchGet([]string{"id1", "id2", "id3"})

// Batch save
err := UserRepo.BatchSave(users)

// Batch delete
err := UserRepo.BatchDelete([]string{"id1", "id2", "id3"})

See Batch Operations for details.

Best Practices

Do

  • Define repositories as global variables
  • Use meaningful repository names (UserRepository, OrderRepository)
  • Leverage auto-table names (only override when necessary)
  • Use soft deletes for audit trails
  • Consider GSIs early for common query patterns

Don't

  • Create multiple repository instances for the same entity
  • Bypass the repository to access DynamoDB directly
  • Forget to handle errors

Complete Example

package main

import (
    "fmt"
    "log"

    "github.com/go-gamma/dynorm"
)

// Entity
type Product struct {
    dynorm.Entity

    Name        string  `dynorm:"gsi:ByName"`
    Category    string  `dynorm:"gsi:ByCategory"`
    Price       float64
    InStock     bool
    Description string
}

// Repository
type ProductRepository struct {
    dynorm.Repository[Product]
}

var ProductRepo = &ProductRepository{}

func main() {
    // Create
    product := &Product{
        Name:        "Laptop",
        Category:    "Electronics",
        Price:       999.99,
        InStock:     true,
        Description: "High-performance laptop",
    }

    if err := ProductRepo.Save(product); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Created product: %s\n", product.ID)

    // Read
    found, err := ProductRepo.Find(product.ID)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Found: %s - $%.2f\n", found.Name, found.Price)

    // Update
    product.Price = 899.99
    if err := ProductRepo.Save(product); err != nil {
        log.Fatal(err)
    }

    // Query
    electronics, err := ProductRepo.
        Where("Category", "=", "Electronics").
        Where("InStock", "=", true).
        OrderBy("Price", "asc").
        GetAll()

    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Found %d electronics in stock\n", electronics.Count())

    // Delete
    if err := ProductRepo.Delete(product); err != nil {
        log.Fatal(err)
    }
}

Next Steps