Files
dancefinder/domain
argoyle b63f3f0070 feat(graph): move resolvers to graph package and update config
Moves resolver implementation from the pkg to the graph package for
better organization. Updates gqlgen.yml configuration to align with
the new structure and ensures that models point to the correct
locations. This change improves maintainability and clarity of the
codebase.
2025-11-06 10:58:22 +01:00
..

DanceFinder Domain Package

This package contains the core domain model for the DanceFinder application, following Domain-Driven Design (DDD) and Event Sourcing principles.

Architecture

The domain package uses the eventsourced framework to implement:

  • Aggregates - Domain entities that are event-sourced
  • Domain Events - Immutable facts about what happened
  • Commands - Intentions to change state

Note: Repositories are located in the separate repositories package.

Naming Conventions

Following DDD best practices, we do not use suffixes like Aggregate or Event:

  • Band (not BandAggregate)
  • BandCreated (not BandCreatedEvent)
  • CreateBand (not CreateBandCommand)

The domain model is self-documenting through types and context.

Aggregates

Band

Represents a musical band/performer.

Events:

  • BandCreated - A band was registered in the system

Commands:

  • CreateBand - Register a new band

DanceHall

Represents a venue where dance events occur.

Events:

  • DanceHallCreated - A dance hall was registered
  • DanceHallGeocoded - Geographic coordinates were added

Commands:

  • CreateDanceHall - Register a new dance hall
  • GeocodeDanceHall - Add geographic coordinates

Event

Represents a dance event (performance at a venue on a specific date).

Events:

  • EventCreated - A dance event was scheduled
  • EventUpdated - Event details were modified
  • EventDeleted - An event was cancelled/removed

Commands:

  • CreateEvent - Schedule a new dance event
  • UpdateEvent - Modify event details
  • DeleteEvent - Cancel/remove an event

UserPreferences

Represents a user's preferences (origins, ignored items).

Events:

  • OriginAdded / OriginRemoved - User's home locations
  • BandIgnored / BandUnignored - Filtered bands
  • DanceHallIgnored / DanceHallUnignored - Filtered venues
  • CityIgnored / CityUnignored - Filtered cities
  • MunicipalityIgnored / MunicipalityUnignored - Filtered municipalities
  • StateIgnored / StateUnignored - Filtered states

Commands:

  • AddOrigin / RemoveOrigin
  • IgnoreBand / UnignoreBand
  • (similar for DanceHall, City, Municipality, State)

Usage Example

package main

import (
    "context"
    "gitlab.com/unboundsoftware/dancefinder/domain"
    "gitlab.com/unboundsoftware/dancefinder/repositories"
    "gitlab.com/unboundsoftware/eventsourced/eventsourced"
    "gitlab.com/unboundsoftware/eventsourced/pg"
    "gitlab.com/unboundsoftware/eventsourced/amqp"
)

func main() {
    ctx := context.Background()

    // Setup event store
    eventStore, _ := pg.New(ctx, db,
        pg.WithEventTypes(
            &domain.BandCreated{},
            &domain.DanceHallCreated{},
            &domain.DanceHallGeocoded{},
            &domain.EventCreated{},
            &domain.EventUpdated{},
            &domain.EventDeleted{},
            &domain.OriginAdded{},
            &domain.OriginRemoved{},
            &domain.BandIgnored{},
            &domain.BandUnignored{},
            // ... all event types
        ),
    )

    // Setup event publisher
    amqpConn, _ := amqp.New(rabbitMQConn)

    // Create repositories
    bandRepo := repositories.NewBandRepository(eventStore, amqpConn)

    // Create a band using a command
    cmd := &domain.CreateBand{
        Name: "Lasse Stefanz",
    }

    band, err := bandRepo.Create(ctx, cmd)
    if err != nil {
        panic(err)
    }

    // Load the band later
    loaded, err := bandRepo.Load(ctx, band.Identity())
    if err != nil {
        panic(err)
    }
}

Event Flow

  1. Command is created - User intention (e.g., CreateBand)
  2. Command is validated - Business rules checked
  3. Event is produced - Command creates a domain event
  4. Event is stored - Persisted in event store
  5. Event is published - Sent to AMQP/RabbitMQ
  6. Event is applied - Aggregate state is updated
  7. Projections updated - Read models are updated via readview package

Integration with Read Models

The domain package focuses on write models (commands and events). Read models (queries) are handled by:

  1. Event Store - Source of truth
  2. readview Package - Automatically updates projections
  3. Projection Tables - Traditional PostgreSQL tables for queries

See the dancefinder service for projection implementations.

Testing

Test aggregates by:

  1. Creating a sequence of events
  2. Applying them to an aggregate
  3. Asserting the resulting state
func TestBandCreation(t *testing.T) {
    band := &domain.Band{}

    event := &domain.BandCreated{
        BaseEvent: eventsourced.BaseEvent{
            EventAggregateId: "band-123",
            EventTime:        time.Now(),
        },
        Name: "Lasse Stefanz",
    }

    err := band.Apply(event)
    require.NoError(t, err)

    assert.Equal(t, "Lasse Stefanz", band.Name)
    assert.Equal(t, "band-123", band.Identity())
}

Migration Strategy

See MIGRATION.md for the complete migration strategy from the current CRUD-based system to this event-sourced domain model.