Skip to content

🎯 Gold Standard Domain Model

A detailed exploration of the domain model powering the Gold Standard module.

The domain model is the heart of the application, containing all business logic independent of any framework or infrastructure concerns.


Domain Model Overview

classDiagram
    class Article {
        +ArticleId id
        +ArticleTitle title
        +ArticleContent content
        +ArticleStatus status
        +AuthorId authorId
        +CategoryId categoryId
        +DateTimeImmutable createdAt
        +DateTimeImmutable? publishedAt
        +create()
        +update()
        +publish()
        +archive()
    }

    class Author {
        +AuthorId id
        +AuthorName name
        +Email email
        +create()
    }

    class Category {
        +CategoryId id
        +CategoryName name
        +Slug slug
        +CategoryId? parentId
        +create()
        +rename()
    }

    class Comment {
        +CommentId id
        +ArticleId articleId
        +AuthorId authorId
        +CommentContent content
        +DateTimeImmutable createdAt
        +create()
        +approve()
        +reject()
    }

    class Tag {
        +TagId id
        +TagName name
        +Slug slug
    }

    Article --> Author : written by
    Article --> Category : belongs to
    Article --> Comment : has many
    Article --> Tag : has many
    Category --> Category : parent

Entities

Entities have identity and lifecycle. They are mutable and encapsulate business rules.

Article Entity

<?php

declare(strict_types=1);

namespace Xoops\GoldStandard\Domain\Entity;

use Xoops\GoldStandard\Domain\ValueObject\{
    ArticleId,
    ArticleTitle,
    ArticleContent,
    ArticleSlug,
    AuthorId,
    CategoryId
};
use Xoops\GoldStandard\Domain\Event\{
    ArticleCreated,
    ArticleUpdated,
    ArticlePublished,
    ArticleArchived
};

final class Article extends AggregateRoot
{
    /** @var list<Tag> */
    private array $tags = [];

    private function __construct(
        public readonly ArticleId $id,
        private ArticleTitle $title,
        private ArticleSlug $slug,
        private ArticleContent $content,
        private ArticleStatus $status,
        private readonly AuthorId $authorId,
        private CategoryId $categoryId,
        private readonly \DateTimeImmutable $createdAt,
        private ?\DateTimeImmutable $updatedAt = null,
        private ?\DateTimeImmutable $publishedAt = null,
    ) {}

    /**
     * Factory method for creating new articles
     */
    public static function create(
        ArticleId $id,
        ArticleTitle $title,
        ArticleContent $content,
        AuthorId $authorId,
        CategoryId $categoryId,
    ): self {
        $article = new self(
            id: $id,
            title: $title,
            slug: ArticleSlug::fromTitle($title),
            content: $content,
            status: ArticleStatus::Draft,
            authorId: $authorId,
            categoryId: $categoryId,
            createdAt: new \DateTimeImmutable(),
        );

        $article->recordEvent(new ArticleCreated(
            articleId: $id,
            title: $title,
            authorId: $authorId,
        ));

        return $article;
    }

    /**
     * Update article content
     */
    public function update(
        ArticleTitle $title,
        ArticleContent $content,
        CategoryId $categoryId,
    ): void {
        $this->title = $title;
        $this->slug = ArticleSlug::fromTitle($title);
        $this->content = $content;
        $this->categoryId = $categoryId;
        $this->updatedAt = new \DateTimeImmutable();

        $this->recordEvent(new ArticleUpdated(
            articleId: $this->id,
            title: $title,
        ));
    }

    /**
     * Publish the article
     */
    public function publish(): void
    {
        $this->guardAgainstInvalidTransition(ArticleStatus::Published);

        $this->status = ArticleStatus::Published;
        $this->publishedAt = new \DateTimeImmutable();
        $this->updatedAt = new \DateTimeImmutable();

        $this->recordEvent(new ArticlePublished(
            articleId: $this->id,
            publishedAt: $this->publishedAt,
        ));
    }

    /**
     * Archive the article
     */
    public function archive(): void
    {
        $this->guardAgainstInvalidTransition(ArticleStatus::Archived);

        $this->status = ArticleStatus::Archived;
        $this->updatedAt = new \DateTimeImmutable();

        $this->recordEvent(new ArticleArchived($this->id));
    }

    /**
     * Add a tag to the article
     */
    public function addTag(Tag $tag): void
    {
        if ($this->hasTag($tag)) {
            return;
        }

        $this->tags[] = $tag;
    }

    /**
     * Remove a tag from the article
     */
    public function removeTag(Tag $tag): void
    {
        $this->tags = array_filter(
            $this->tags,
            fn(Tag $t) => !$t->id->equals($tag->id)
        );
    }

    public function hasTag(Tag $tag): bool
    {
        foreach ($this->tags as $t) {
            if ($t->id->equals($tag->id)) {
                return true;
            }
        }
        return false;
    }

    // Getters
    public function getTitle(): ArticleTitle { return $this->title; }
    public function getSlug(): ArticleSlug { return $this->slug; }
    public function getContent(): ArticleContent { return $this->content; }
    public function getStatus(): ArticleStatus { return $this->status; }
    public function getAuthorId(): AuthorId { return $this->authorId; }
    public function getCategoryId(): CategoryId { return $this->categoryId; }
    public function getCreatedAt(): \DateTimeImmutable { return $this->createdAt; }
    public function getUpdatedAt(): ?\DateTimeImmutable { return $this->updatedAt; }
    public function getPublishedAt(): ?\DateTimeImmutable { return $this->publishedAt; }
    /** @return list<Tag> */
    public function getTags(): array { return $this->tags; }

    public function isPublished(): bool
    {
        return $this->status === ArticleStatus::Published;
    }

    public function isDraft(): bool
    {
        return $this->status === ArticleStatus::Draft;
    }

    private function guardAgainstInvalidTransition(ArticleStatus $to): void
    {
        if (!$this->status->canTransitionTo($to)) {
            throw new InvalidStatusTransition(
                "Cannot transition from {$this->status->value} to {$to->value}"
            );
        }
    }
}

Article Status Enum

<?php

declare(strict_types=1);

namespace Xoops\GoldStandard\Domain\Entity;

enum ArticleStatus: string
{
    case Draft = 'draft';
    case Published = 'published';
    case Archived = 'archived';

    public function canTransitionTo(self $to): bool
    {
        return match($this) {
            self::Draft => in_array($to, [self::Published, self::Archived]),
            self::Published => $to === self::Archived,
            self::Archived => false,
        };
    }

    public function label(): string
    {
        return match($this) {
            self::Draft => 'Draft',
            self::Published => 'Published',
            self::Archived => 'Archived',
        };
    }

    public function color(): string
    {
        return match($this) {
            self::Draft => 'gray',
            self::Published => 'green',
            self::Archived => 'red',
        };
    }
}

State Machine Visualization

stateDiagram-v2
    [*] --> Draft: create()

    Draft --> Published: publish()
    Draft --> Archived: archive()
    Published --> Archived: archive()

    Draft: Can be edited
    Draft: Not visible to public

    Published: Visible to public
    Published: Can only be archived

    Archived: No longer visible
    Archived: Cannot transition

Value Objects

Value Objects are immutable and defined by their attributes, not identity.

ArticleId

Uses XMF ULID (Universally Unique Lexicographically Sortable Identifier) for identifiers. ULIDs are preferable to UUIDs for database indexing since they're time-ordered and more compact.

<?php

declare(strict_types=1);

namespace Xoops\GoldStandard\Domain\ValueObject;

use Xmf\Ulid;

/**
 * Article identifier using XMF ULID
 *
 * ULIDs offer advantages over UUIDs:
 * - Lexicographically sortable (time-ordered)
 * - 26 characters vs 36 for UUID
 * - URL-safe (Crockford's Base32)
 * - Monotonic within same millisecond
 */
final readonly class ArticleId
{
    private function __construct(
        private Ulid $value,
    ) {}

    /**
     * Generate a new unique identifier
     */
    public static function generate(): self
    {
        return new self(Ulid::generate());
    }

    /**
     * Create from string representation
     *
     * @throws InvalidArticleId If string is not a valid ULID
     */
    public static function fromString(string $id): self
    {
        if (!Ulid::isValid($id)) {
            throw new InvalidArticleId("Invalid article ID: {$id}");
        }
        return new self(Ulid::fromString($id));
    }

    /**
     * Get string representation (26 characters)
     */
    public function toString(): string
    {
        return $this->value->toString();
    }

    /**
     * Get the timestamp when this ID was created
     */
    public function getTimestamp(): \DateTimeImmutable
    {
        return $this->value->getTimestamp();
    }

    /**
     * Compare with another ArticleId
     */
    public function equals(self $other): bool
    {
        return $this->value->equals($other->value);
    }

    /**
     * Compare for ordering (ULIDs are lexicographically sortable)
     */
    public function compareTo(self $other): int
    {
        return $this->value->compareTo($other->value);
    }

    public function __toString(): string
    {
        return $this->toString();
    }
}

ULID Format: 01ARZ3NDEKTSV4RRFFQ69G5FAV (26 characters)

flowchart LR
    subgraph ULID["ULID Structure (26 chars)"]
        TS[Timestamp<br/>10 chars<br/>48-bit ms]
        RND[Randomness<br/>16 chars<br/>80-bit]
    end

    TS --> RND
Feature ULID UUID v4 UUID v7
Length 26 36 36
Sortable
URL-safe
Time-extractable
Database Index Performance Excellent Poor Good

AuthorId

<?php

declare(strict_types=1);

namespace Xoops\GoldStandard\Domain\ValueObject;

use Xmf\Ulid;

/**
 * Author identifier using XMF ULID
 */
final readonly class AuthorId
{
    private function __construct(
        private Ulid $value,
    ) {}

    public static function generate(): self
    {
        return new self(Ulid::generate());
    }

    public static function fromString(string $id): self
    {
        if (!Ulid::isValid($id)) {
            throw new InvalidAuthorId("Invalid author ID: {$id}");
        }
        return new self(Ulid::fromString($id));
    }

    public function toString(): string
    {
        return $this->value->toString();
    }

    public function getTimestamp(): \DateTimeImmutable
    {
        return $this->value->getTimestamp();
    }

    public function equals(self $other): bool
    {
        return $this->value->equals($other->value);
    }

    public function compareTo(self $other): int
    {
        return $this->value->compareTo($other->value);
    }

    public function __toString(): string
    {
        return $this->toString();
    }
}

CategoryId

<?php

declare(strict_types=1);

namespace Xoops\GoldStandard\Domain\ValueObject;

use Xmf\Ulid;

/**
 * Category identifier using XMF ULID
 */
final readonly class CategoryId
{
    private function __construct(
        private Ulid $value,
    ) {}

    public static function generate(): self
    {
        return new self(Ulid::generate());
    }

    public static function fromString(string $id): self
    {
        if (!Ulid::isValid($id)) {
            throw new InvalidCategoryId("Invalid category ID: {$id}");
        }
        return new self(Ulid::fromString($id));
    }

    public function toString(): string
    {
        return $this->value->toString();
    }

    public function getTimestamp(): \DateTimeImmutable
    {
        return $this->value->getTimestamp();
    }

    public function equals(self $other): bool
    {
        return $this->value->equals($other->value);
    }

    public function compareTo(self $other): int
    {
        return $this->value->compareTo($other->value);
    }

    public function __toString(): string
    {
        return $this->toString();
    }
}

CommentId

<?php

declare(strict_types=1);

namespace Xoops\GoldStandard\Domain\ValueObject;

use Xmf\Ulid;

/**
 * Comment identifier using XMF ULID
 */
final readonly class CommentId
{
    private function __construct(
        private Ulid $value,
    ) {}

    public static function generate(): self
    {
        return new self(Ulid::generate());
    }

    public static function fromString(string $id): self
    {
        if (!Ulid::isValid($id)) {
            throw new InvalidCommentId("Invalid comment ID: {$id}");
        }
        return new self(Ulid::fromString($id));
    }

    public function toString(): string
    {
        return $this->value->toString();
    }

    public function getTimestamp(): \DateTimeImmutable
    {
        return $this->value->getTimestamp();
    }

    public function equals(self $other): bool
    {
        return $this->value->equals($other->value);
    }

    public function compareTo(self $other): int
    {
        return $this->value->compareTo($other->value);
    }

    public function __toString(): string
    {
        return $this->toString();
    }
}

TagId

<?php

declare(strict_types=1);

namespace Xoops\GoldStandard\Domain\ValueObject;

use Xmf\Ulid;

/**
 * Tag identifier using XMF ULID
 */
final readonly class TagId
{
    private function __construct(
        private Ulid $value,
    ) {}

    public static function generate(): self
    {
        return new self(Ulid::generate());
    }

    public static function fromString(string $id): self
    {
        if (!Ulid::isValid($id)) {
            throw new InvalidTagId("Invalid tag ID: {$id}");
        }
        return new self(Ulid::fromString($id));
    }

    public function toString(): string
    {
        return $this->value->toString();
    }

    public function getTimestamp(): \DateTimeImmutable
    {
        return $this->value->getTimestamp();
    }

    public function equals(self $other): bool
    {
        return $this->value->equals($other->value);
    }

    public function compareTo(self $other): int
    {
        return $this->value->compareTo($other->value);
    }

    public function __toString(): string
    {
        return $this->toString();
    }
}

Note: All ID value objects follow the same pattern using XMF ULID. This ensures consistency across the domain model and leverages the benefits of lexicographically sortable, time-ordered identifiers.

To reduce code duplication, use the Xmf\EntityId trait:

<?php

declare(strict_types=1);

namespace Xoops\GoldStandard\Domain\ValueObject;

use Xmf\EntityId;

/**
 * Article identifier using XMF EntityId trait
 *
 * The trait provides all common ULID functionality:
 * - generate(), fromString(), fromBinary()
 * - toString(), toBinary(), getTimestamp()
 * - equals(), compareTo(), isBefore(), isAfter()
 */
final readonly class ArticleId implements \Stringable, \JsonSerializable
{
    use EntityId;

    protected static function exceptionClass(): string
    {
        return InvalidArticleId::class;
    }
}

// Same pattern for all other IDs:
final readonly class AuthorId implements \Stringable, \JsonSerializable
{
    use EntityId;

    protected static function exceptionClass(): string
    {
        return InvalidAuthorId::class;
    }
}

final readonly class CategoryId implements \Stringable, \JsonSerializable
{
    use EntityId;

    protected static function exceptionClass(): string
    {
        return InvalidCategoryId::class;
    }
}

final readonly class CommentId implements \Stringable, \JsonSerializable
{
    use EntityId;

    protected static function exceptionClass(): string
    {
        return InvalidCommentId::class;
    }
}

final readonly class TagId implements \Stringable, \JsonSerializable
{
    use EntityId;

    protected static function exceptionClass(): string
    {
        return InvalidTagId::class;
    }
}

This approach: - Reduces duplication: ~40 lines per ID class → ~10 lines - Ensures consistency: All IDs behave identically - Simplifies maintenance: Fix once, apply everywhere - Type safety preserved: Each ID is still a distinct type

ArticleTitle

<?php

declare(strict_types=1);

namespace Xoops\GoldStandard\Domain\ValueObject;

final readonly class ArticleTitle
{
    private const MIN_LENGTH = 3;
    private const MAX_LENGTH = 255;

    private function __construct(
        public string $value,
    ) {}

    public static function fromString(string $title): self
    {
        $title = trim($title);

        if ($title === '') {
            throw new InvalidArticleTitle('Title cannot be empty');
        }

        if (mb_strlen($title) < self::MIN_LENGTH) {
            throw new InvalidArticleTitle(
                sprintf('Title must be at least %d characters', self::MIN_LENGTH)
            );
        }

        if (mb_strlen($title) > self::MAX_LENGTH) {
            throw new InvalidArticleTitle(
                sprintf('Title cannot exceed %d characters', self::MAX_LENGTH)
            );
        }

        return new self($title);
    }

    public function equals(self $other): bool
    {
        return $this->value === $other->value;
    }

    public function __toString(): string
    {
        return $this->value;
    }
}

ArticleContent

<?php

declare(strict_types=1);

namespace Xoops\GoldStandard\Domain\ValueObject;

final readonly class ArticleContent
{
    private const MIN_LENGTH = 50;
    private const MAX_LENGTH = 100000;

    private function __construct(
        public string $value,
        public string $format,
    ) {}

    public static function fromString(
        string $content,
        string $format = 'html',
    ): self {
        $content = trim($content);

        if (mb_strlen($content) < self::MIN_LENGTH) {
            throw new InvalidArticleContent(
                sprintf('Content must be at least %d characters', self::MIN_LENGTH)
            );
        }

        if (mb_strlen($content) > self::MAX_LENGTH) {
            throw new InvalidArticleContent(
                sprintf('Content cannot exceed %d characters', self::MAX_LENGTH)
            );
        }

        return new self($content, $format);
    }

    public function excerpt(int $length = 200): string
    {
        $text = strip_tags($this->value);
        if (mb_strlen($text) <= $length) {
            return $text;
        }
        return mb_substr($text, 0, $length) . '...';
    }

    public function wordCount(): int
    {
        return str_word_count(strip_tags($this->value));
    }

    public function readingTime(int $wordsPerMinute = 200): int
    {
        return (int) ceil($this->wordCount() / $wordsPerMinute);
    }
}

ArticleSlug

Uses XMF Slug for URL-friendly identifier generation. XMF provides robust slug handling with proper Unicode transliteration and collision handling.

<?php

declare(strict_types=1);

namespace Xoops\GoldStandard\Domain\ValueObject;

use Xmf\Slug;

/**
 * Article slug using XMF Slug
 *
 * XMF Slug provides:
 * - Proper Unicode to ASCII transliteration
 * - Multi-language support (CJK, Cyrillic, Arabic, etc.)
 * - Configurable separator and length
 * - Built-in uniqueness suffix support
 */
final readonly class ArticleSlug
{
    private const MAX_LENGTH = 100;
    private const SEPARATOR = '-';

    private function __construct(
        private Slug $slug,
    ) {}

    /**
     * Create from existing slug string
     *
     * @throws InvalidArticleSlug If string is not a valid slug
     */
    public static function fromString(string $slug): self
    {
        $normalized = Slug::normalize($slug);

        if ($normalized === '') {
            throw new InvalidArticleSlug('Slug cannot be empty');
        }

        if (!Slug::isValid($normalized)) {
            throw new InvalidArticleSlug('Invalid slug format');
        }

        return new self(Slug::fromString($normalized));
    }

    /**
     * Generate slug from article title
     */
    public static function fromTitle(ArticleTitle $title): self
    {
        $slug = Slug::create($title->value, [
            'separator' => self::SEPARATOR,
            'maxLength' => self::MAX_LENGTH,
            'lowercase' => true,
        ]);

        return new self($slug);
    }

    /**
     * Create a unique variant by appending a suffix
     *
     * @param int $suffix Number to append (e.g., 2 for "my-article-2")
     */
    public function withSuffix(int $suffix): self
    {
        $newSlug = $this->slug->withSuffix($suffix, self::SEPARATOR);
        return new self($newSlug);
    }

    /**
     * Get string representation
     */
    public function toString(): string
    {
        return $this->slug->toString();
    }

    /**
     * Get the value property for direct access
     */
    public string $value {
        get => $this->slug->toString();
    }

    public function equals(self $other): bool
    {
        return $this->slug->equals($other->slug);
    }

    public function __toString(): string
    {
        return $this->toString();
    }
}

Slug Features:

Feature Description
Unicode Support Handles CJK, Cyrillic, Arabic, accented chars
Transliteration "日本語" → "ri-ben-yu"
Length Control Truncates at word boundaries
Uniqueness Built-in suffix support for duplicates
flowchart LR
    TITLE["Article Title"] --> SLUG["XMF Slug"]
    SLUG --> NORM[Normalize]
    NORM --> TRANS[Transliterate]
    TRANS --> LOWER[Lowercase]
    LOWER --> SEP[Separator]
    SEP --> TRIM[Trim/Length]
    TRIM --> OUT["url-friendly-slug"]

Aggregate Root

The base class for aggregate roots that collect domain events.

<?php

declare(strict_types=1);

namespace Xoops\GoldStandard\Domain\Entity;

abstract class AggregateRoot
{
    /** @var list<object> */
    private array $domainEvents = [];

    protected function recordEvent(object $event): void
    {
        $this->domainEvents[] = $event;
    }

    /**
     * Pull and clear all recorded domain events
     * @return list<object>
     */
    public function pullDomainEvents(): array
    {
        $events = $this->domainEvents;
        $this->domainEvents = [];
        return $events;
    }

    /**
     * Check if there are pending domain events
     */
    public function hasDomainEvents(): bool
    {
        return count($this->domainEvents) > 0;
    }
}

Domain Events

Events that represent significant occurrences in the domain.

flowchart LR
    subgraph Events["Domain Events"]
        E1[ArticleCreated]
        E2[ArticleUpdated]
        E3[ArticlePublished]
        E4[ArticleArchived]
        E5[ArticleDeleted]
    end

    ART[Article] -->|records| Events

ArticleCreated Event

<?php

declare(strict_types=1);

namespace Xoops\GoldStandard\Domain\Event;

final readonly class ArticleCreated implements DomainEvent
{
    public function __construct(
        public ArticleId $articleId,
        public ArticleTitle $title,
        public AuthorId $authorId,
        public \DateTimeImmutable $occurredAt = new \DateTimeImmutable(),
    ) {}

    public function aggregateId(): string
    {
        return $this->articleId->toString();
    }
}

ArticlePublished Event

<?php

declare(strict_types=1);

namespace Xoops\GoldStandard\Domain\Event;

final readonly class ArticlePublished implements DomainEvent
{
    public function __construct(
        public ArticleId $articleId,
        public \DateTimeImmutable $publishedAt,
        public \DateTimeImmutable $occurredAt = new \DateTimeImmutable(),
    ) {}

    public function aggregateId(): string
    {
        return $this->articleId->toString();
    }
}

Domain Services

Services that contain domain logic not naturally belonging to entities.

SlugUniquenessChecker

<?php

declare(strict_types=1);

namespace Xoops\GoldStandard\Domain\Service;

interface SlugUniquenessChecker
{
    public function isUnique(ArticleSlug $slug, ?ArticleId $excludeId = null): bool;
}

ArticlePublishingPolicy

<?php

declare(strict_types=1);

namespace Xoops\GoldStandard\Domain\Service;

final class ArticlePublishingPolicy
{
    public function __construct(
        private readonly int $minimumWordCount = 100,
        private readonly bool $requireCategory = true,
        private readonly bool $requireTags = false,
        private readonly int $minimumTags = 1,
    ) {}

    public function canPublish(Article $article): PublishingResult
    {
        $violations = [];

        if ($article->getContent()->wordCount() < $this->minimumWordCount) {
            $violations[] = sprintf(
                'Article must have at least %d words',
                $this->minimumWordCount
            );
        }

        if ($this->requireCategory && $article->getCategoryId() === null) {
            $violations[] = 'Article must have a category';
        }

        if ($this->requireTags && count($article->getTags()) < $this->minimumTags) {
            $violations[] = sprintf(
                'Article must have at least %d tags',
                $this->minimumTags
            );
        }

        return new PublishingResult(
            canPublish: empty($violations),
            violations: $violations,
        );
    }
}

Repository Interfaces

Contracts for data access, defined in the domain layer.

<?php

declare(strict_types=1);

namespace Xoops\GoldStandard\Domain\Repository;

interface ArticleRepository
{
    public function nextIdentity(): ArticleId;

    public function find(ArticleId $id): ?Article;

    public function findOrFail(ArticleId $id): Article;

    public function findBySlug(ArticleSlug $slug): ?Article;

    /** @return list<Article> */
    public function findByStatus(ArticleStatus $status): array;

    /** @return list<Article> */
    public function findByAuthor(AuthorId $authorId): array;

    /** @return list<Article> */
    public function findByCategory(CategoryId $categoryId): array;

    public function save(Article $article): void;

    public function remove(Article $article): void;

    public function count(): int;

    public function countByStatus(ArticleStatus $status): int;
}

Domain Model Diagram

flowchart TB
    subgraph Domain["Domain Layer"]
        subgraph Entities
            ART[Article]
            CAT[Category]
            TAG[Tag]
            CMT[Comment]
        end

        subgraph ValueObjects["Value Objects"]
            ID[ArticleId]
            TITLE[ArticleTitle]
            CONTENT[ArticleContent]
            SLUG[ArticleSlug]
            STATUS[ArticleStatus]
        end

        subgraph Events["Domain Events"]
            E1[ArticleCreated]
            E2[ArticlePublished]
        end

        subgraph Services["Domain Services"]
            PUB[PublishingPolicy]
            UNIQUE[SlugUniqueness]
        end

        subgraph Repositories["Repository Interfaces"]
            REPO[ArticleRepository]
        end
    end

    ART --> ValueObjects
    ART -->|records| Events
    Services --> ART
    REPO --> ART


domain-model #ddd #entities #value-objects #goldstandard