Use case driven EventSourcing - Let go of the Aggregate with the Dynamic Consistency Boundary (DCB) pattern.
- Background
- Installation
- Usage
- Use cases / aggregates - Model business logic using event-sourced use cases and traditional aggregates with DCB (Domain Centric Business logic) or aggregate patterns
- Command handlers - Trigger behavioral actions on use cases using command handlers
- Domain events - Define and work with domain events, including naming, serialization, and domain tags
- Sagas - Implement long-running business processes that coordinate complex workflows across multiple domain events
- Library architecture
- Library reference
- Hooking into the library
Domain concepts are modeled towards objects: the aggregate.
- Any business logic related to a single domain object should live inside the aggregate
- Logic that involves other domain objects or groups of the same kind of domain objects does not belong in the aggregate
Domain concepts are modeled through use cases.
- Any business logic tied to a use case should live inside that use case
- A use case can relate to one or more domain concepts
This example demonstrates all key features of the library: a DCB use case with command handler, domain events, and a saga coordinating a workflow.
Scenario: A student subscribes to a course. When subscription succeeds, a saga automatically sends a welcome email.
use Gember\EventSourcing\UseCase\Attribute\DomainEvent;
use Gember\EventSourcing\UseCase\Attribute\DomainTag;
use Gember\EventSourcing\Saga\Attribute\SagaId;
#[DomainEvent(name: 'course.created')]
final readonly class CourseCreatedEvent
{
public function __construct(
#[DomainTag]
public string $courseId,
public string $name,
) {}
}
#[DomainEvent(name: 'student.registered')]
final readonly class StudentRegisteredEvent
{
public function __construct(
#[DomainTag]
public string $studentId,
public string $email,
) {}
}
#[DomainEvent(name: 'student.subscribed')]
final readonly class StudentSubscribedEvent
{
public function __construct(
#[DomainTag]
#[SagaId] // Links to SubscriptionWelcomeSaga
public string $courseId,
#[DomainTag]
#[SagaId]
public string $studentId,
) {}
}
use Gember\EventSourcing\Common\CreationPolicy;
use Gember\EventSourcing\UseCase\Attribute\DomainCommandHandler;
use Gember\EventSourcing\UseCase\Attribute\DomainEventSubscriber;
use Gember\EventSourcing\UseCase\Attribute\DomainTag;
use Gember\EventSourcing\UseCase\EventSourcedUseCase;
use Gember\EventSourcing\UseCase\EventSourcedUseCaseBehaviorTrait;
final class SubscribeStudentToCourse implements EventSourcedUseCase
{
use EventSourcedUseCaseBehaviorTrait;
#[DomainTag]
private CourseId $courseId;
#[DomainTag]
private StudentId $studentId;
private bool $isSubscribed = false;
/**
* Subscribes a student to a course (DCB pattern with multiple domain tags).
* Uses __invoke to emphasize this is a single-purpose use case.
*/
#[DomainCommandHandler(policy: CreationPolicy::IfMissing)]
public function __invoke(SubscribeStudentCommand $command): void
{
// 1. Check idempotency
if ($this->isSubscribed) {
return;
}
// 2. Protect invariants (simplified for example)
// In real scenarios: check capacity, prerequisites, etc.
// 3. Apply domain event
$this->apply(new StudentSubscribedEvent(
$command->courseId,
$command->studentId,
));
}
#[DomainEventSubscriber]
private function onCourseCreated(CourseCreatedEvent $event): void
{
$this->courseId = new CourseId($event->courseId);
}
#[DomainEventSubscriber]
private function onStudentRegistered(StudentRegisteredEvent $event): void
{
$this->studentId = new StudentId($event->studentId);
}
#[DomainEventSubscriber]
private function onStudentSubscribed(StudentSubscribedEvent $event): void
{
$this->isSubscribed = true;
}
}
use Gember\DependencyContracts\Util\Messaging\MessageBus\CommandBus;
use Gember\EventSourcing\Common\CreationPolicy;
use Gember\EventSourcing\Saga\Attribute\Saga;
use Gember\EventSourcing\Saga\Attribute\SagaEventSubscriber;
use Gember\EventSourcing\Saga\Attribute\SagaId;
#[Saga(name: 'subscription.welcome')]
final class SubscriptionWelcomeSaga
{
#[SagaId]
public ?string $courseId = null;
#[SagaId]
public ?string $studentId = null;
private bool $welcomeEmailSent = false;
/**
* When a student subscribes, automatically send a welcome email.
*/
#[SagaEventSubscriber(policy: CreationPolicy::IfMissing)]
public function onStudentSubscribed(StudentSubscribedEvent $event, CommandBus $commandBus): void
{
$this->courseId = $event->courseId;
$this->studentId = $event->studentId;
// Dispatch command to send welcome email
$commandBus->handle(new SendWelcomeEmailCommand(
$event->studentId,
$event->courseId,
));
$this->welcomeEmailSent = true;
}
}
For more extended examples and complete implementations, check out the demo application gember/example-event-sourcing-dcb.