Skip to content

Entity Pattern

Entities are the core data structures in Dynorm. They represent your DynamoDB items with automatic ID generation, timestamps, and lifecycle management.

Base Entity

All entities embed dynorm.Entity which provides:

type Entity struct {
    ID        string     `dynorm:"pk"`        // ULID, auto-generated
    CreatedAt time.Time  `dynorm:"createdAt"` // Derived from ULID timestamp
    UpdatedAt time.Time  `dynorm:"updatedAt"` // Set on every save
    DeletedAt *time.Time `dynorm:"deletedAt"` // For soft deletes (optional)
}

Defining Entities

Focus only on your business fields - Dynorm handles the rest:

type User struct {
    dynorm.Entity

    // Business fields only
    Email     string `dynorm:"gsi:ByEmail"`
    Username  string `dynorm:"gsi:ByUsername"`
    FirstName string
    LastName  string
    Password  string // hashed
    Status    string
    Role      string
}

type Post struct {
    dynorm.Entity `soft_deletes:"true"` // Enable soft deletes

    Title     string
    Content   string
    AuthorID  string `dynorm:"gsi:ByAuthor"`
    Published bool
    Tags      []string
}

ULID-Based IDs

Dynorm uses ULID (Universally Unique Lexicographically Sortable Identifier) for IDs:

user := &User{Email: "test@example.com"}
err := UserRepo.Save(user)

fmt.Println(user.ID)
// Output: 01HQ3K4N5M6P7Q8R9S0T1U2V3W

Why ULID?

Feature ULID UUID v4 Auto-increment
Sortable
Distributed
URL-safe
Contains timestamp
Size 26 chars 36 chars Variable

ULID Properties

  • 26 characters, alphanumeric (0-9, A-Z)
  • Lexicographically sortable (time-ordered)
  • Contains millisecond timestamp
  • Globally unique
  • URL-safe

Entity Lifecycle

stateDiagram-v2
    [*] --> New: Create struct
    New --> Saved: Save()
    Saved --> Updated: Save()
    Updated --> Updated: Save()
    Updated --> SoftDeleted: SoftDelete()
    SoftDeleted --> Updated: Restore()
    SoftDeleted --> [*]: HardDelete()
    Updated --> [*]: Delete()

Lifecycle Methods

// Check if entity is new (no ID yet)
if user.IsNew() {
    fmt.Println("This is a new user")
}

// Check if soft deleted
if post.IsDeleted() {
    fmt.Println("This post is deleted")
}

// Get timestamps
fmt.Printf("Created: %s\n", user.GetCreatedAt())
fmt.Printf("Updated: %s\n", user.GetUpdatedAt())

// Soft delete
post.SoftDelete()
err := PostRepo.Save(post)

// Restore soft deleted entity
post.Restore()
err := PostRepo.Save(post)

Timestamps

CreatedAt

Derived from the ULID timestamp when the entity is first saved:

user := &User{Email: "test@example.com"}
fmt.Println(user.CreatedAt) // 0001-01-01 00:00:00 (zero value)

err := UserRepo.Save(user)
fmt.Println(user.CreatedAt) // 2024-01-15 10:30:45.123 (from ULID)

UpdatedAt

Automatically set to current time on every save:

user, _ := UserRepo.Find("01HQ3K...")
fmt.Println(user.UpdatedAt) // 2024-01-15 10:30:45

user.Status = "verified"
err := UserRepo.Save(user)
fmt.Println(user.UpdatedAt) // 2024-01-15 11:45:30 (updated)

Soft Deletes

Enable soft deletes to mark entities as deleted without removing them:

type Post struct {
    dynorm.Entity `soft_deletes:"true"`

    Title   string
    Content string
}

Using Soft Deletes

// Soft delete a post
post.SoftDelete()
err := PostRepo.Save(post)

// Post still exists in database with DeletedAt set
found, _ := PostRepo.Find(post.ID)
fmt.Println(found.IsDeleted()) // true
fmt.Println(found.DeletedAt)   // 2024-01-15 12:00:00

// Restore the post
post.Restore()
err = PostRepo.Save(post)
fmt.Println(post.IsDeleted()) // false

// Query excluding soft deleted (default behavior)
activePosts, _ := PostRepo.
    Where("Published", "=", true).
    GetAll() // Excludes soft deleted posts

Dirty Tracking

Dynorm tracks changes to entities loaded from the database:

user, _ := UserRepo.Find("01HQ3K...")

// Check if loaded from database
fmt.Println(user.IsLoaded()) // true

// Check if specific field changed
user.Email = "new@example.com"
fmt.Println(user.IsDirty("Email"))     // true
fmt.Println(user.IsDirty("FirstName")) // false

// Check if any field changed
fmt.Println(user.HasChanges()) // true

// Get list of changed fields
fields := user.DirtyFields() // ["Email"]

// Get map of changes
changes := user.Changes() // {"Email": "new@example.com"}

// Get original value
original, ok := user.OriginalValue("Email")
fmt.Println(original) // "old@example.com"

Field Naming

Fields are automatically converted to snake_case:

type User struct {
    dynorm.Entity

    FirstName    string // → first_name
    LastName     string // → last_name
    EmailAddress string // → email_address
    PhoneNumber  string // → phone_number
}

This applies to:

  • DynamoDB attribute names
  • JSON serialization
  • YAML serialization

Complex Types

Dynorm supports various Go types:

type Product struct {
    dynorm.Entity

    Name        string
    Price       float64
    Tags        []string           // Stored as DynamoDB List
    Attributes  map[string]string  // Stored as DynamoDB Map
    InStock     bool
    Quantity    int
    LaunchDate  time.Time          // Stored as RFC3339 string
}

Struct Tags Reference

Tag Description Example
pk Partition key dynorm:"pk"
sk Sort key dynorm:"sk"
gsi:IndexName GSI partition key dynorm:"gsi:ByEmail"
gsi:IndexName:sk GSI sort key dynorm:"gsi:ByEmail:sk"
lsi:IndexName LSI sort key dynorm:"lsi:ByDate"
version Optimistic locking dynorm:"version"
ttl TTL attribute dynorm:"ttl"

Best Practices

Do

  • Keep entities focused on business fields
  • Use meaningful GSI names (ByEmail, ByAuthor)
  • Consider soft deletes for audit trails
  • Let Dynorm generate ULIDs

Don't

  • Override auto-generated IDs
  • Manually modify timestamps
  • Add database logic to entities

Complete Example

type User struct {
    dynorm.Entity `soft_deletes:"true"`

    // Indexed fields
    Email    string `dynorm:"gsi:ByEmail"`
    Username string `dynorm:"gsi:ByUsername"`

    // Profile
    FirstName string
    LastName  string
    Avatar    string

    // Auth
    PasswordHash string
    LastLoginAt  *time.Time

    // Status
    Status    string // active, suspended, banned
    Role      string // user, admin, moderator
    Verified  bool

    // Versioning for optimistic locking
    Version int `dynorm:"version"`
}

Next Steps