The Domain Model: Entity Identity

Updated on @September 20, 2023

Entities, by definition, have an identity, which we can use to save it and get it back from storage again.

$this->orderRepository->ofId($orderId);
Using the OrderRepository to get an Order from storage.

Identity as an auto-incrementing integer

If the ID is an auto-incrementing integer, its value will only be known when the repository has saved the entity.

$orderId = $orderRepository−>save($order);
Using the OrderRepository to save an Order and get its ID.

The real issue is that we're still not replacing the underlying storage technology:

  • Not every database will support auto-incrementing ID columns.
  • Only some databases will be able to generate an ID and return it.
  • Some persistence mechanisms might not return an identifier to the client synchronously.

Another problem is the Entity Consistency; the entity is supposed to be complete from the beginning. Given that an entity only has an ID once saved, we come to the opposite conclusion: the entity is consistent once the database has saved it.

What we'd like instead is a way to provide an entity with an ID the moment we instantiate it.

final class Order 
{
    public function __construct( 
        public readonly int $id,
				// ...
    ) {
    } 
}
Order now has an identity from the start.

Clients will then have to supply an ID upfront when they want to create a new entity. But how could a client find out what the next available ID is? Using the entity's repository method nextIdentity().

interface OrderRepository 
{ 
    public function nextIdentity(): int; 
 
    // ... 
}
nextIdentity() returns the next available ID.

A robust implementation to avoid concurrency issues is using a sequence at the database level.

public function nextIdentity(): int 
{ 
    return $this−>connection−>transactional(function () { 
        $nextId = (int)$this−>connection−>execute( 
            'SELECT last_id FROM order_id_sequence' 
        )−>fetchColumn(0) + 1; 
 
        $this−>connection−>execute( 
            'UPDATE order_id_sequence SET last_id = :last_id', 
            [ 
                'last_id' => $nextId 
            ] 
        ); 
 
        return $nextId; 
    }); 
}
This implementation of nextIdentity() uses a sequence table.

Identity as a UUID

A better alternative to using incrementing integers would be to use a Universally Unique Identifier (UUID). A UUID is often represented as a string but can be converted to a big integer and back again. A UUID is based on the current time and a random number generated by the system's random device.

use Ramsey\Uuid\Uuid; 
use Ramsey\Uuid\UuidInterface; 
 
final class SqlOrderRepository implements OrderRepository 
{
    public function nextIdentity(): UuidInterface 
    { 
        return Uuid::uuid4(); 
    }

		// ...
}
This implementation of nextIdentity() uses the ramsey/uuid library for generating a random UUID.

Strongly Typed IDs: Using a Value Object for the identifier

Now that we're changing an Order's identifier type let's wrap the identifier inside a Value Object. That way, we can fully encapsulate the actual data type of the identifier. Strongly Typed IDs lead us to a more expressive design and solve primitive obsession from entity identifiers.

final class OrderId 
{  
    private function __construct(
				public readonly UuidInterface $id,
		) {
    } 
 
    public static function fromUuid(UuidInterface $id): self 
    { 
        return new self($id); 
    } 
} 
 
final class Order 
{
    public function __construct( 
        public readonly OrderId $id,
        // ...
    ) {
    } 
} 
 
final class SqlOrderRepository implements OrderRepository 
{
    public function nextIdentity(): OrderId 
    { 
        return OrderId::fromUuid(Uuid::uuid4()); 
    } 

		// ...
}
OrderId hides the underlying ID type.

Also, we can delegate the OrderId value object to create a new value, so we can stop using the repository's nextIdentity().

use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;

final class OrderId 
{  
    private function __construct(
				public readonly UuidInterface $id,
		) {
    } 
 
    public static function fromUuid(UuidInterface $id): self 
    { 
        return new self($id); 
    } 

		public static function random(): self 
    { 
        return new self(Uuid::uuid4()); 
    } 
} 

Bibliography