Transform an anemic model into a rich model

using Tactical DDD patterns

Touraine Tech 2025

Hi, I am Arnaud!

Freelancer software craftman

@arnolanglade

Long time ago, I was a

Sylius core team

developer

It helped me to ship CRUD applications more quickly

ionicons-v5-i

I mainly contributed to developing the resource bundle

A Crud works with anemic models

Validator

Service

Serializer

Form

Class without any business logic

Then I became

Akeneo coRe team

developer

Akeneo PIM looks like a big CRUD app, but it isn't

ionicons-v5-i

Building a business-oriented app with anemic models is tricky

Anemic models have their limitations

It is more complicated to describe business problems

You can't ensure business invariants

DDD tactical patterns

to the rescue!

Entities, Value objects and Aggregates

help to design rich domain objects

Domain Driven Design

Do not apply DDD if your application does not involve any business logic

Let's take an example: a map creation

Value object

Entity

Identity-less

Attribute based equality

Immutable

Behavior-rich

Self-validating

Enforce invariant

Focus on behavior (not data)

 Has an identity

Identity based equality

What is an aggregate?

An aggregate is a cluster of objects (entities and value objects) that together represent a domain concept

Map

Marker

Media

Name

Location

Path

aggregate root

entity

entity

Value object

Value object

Name

Name

Marker List

Map Gallery

Maker Gallery

Value object

Value object

Value object

Content

Map

Marker

Media

Cartographer

MarkerCategory

ID

ID

ID

No more Getters and setters

Object = behavior + state

shareWithFriend

addAssetsToGallery

Class

addMarker

Constructor

Internal

State


final class Map
{  
  // Primary constructor
  public function __construct(
    private readonly string $id,
    private readonly Name $name,
    private MarkerList $markers,
  ) {
  }
  
  // Secondary constructor
  public static function create(string $id, string $name): self
  {
    return new self(
      $id, 
      new Name($name), 
      MarkerList::empty()
    );
  }
  
  // ...
}

Build objects with constructors


final class Map
{  
  // ...
  
  public function addMarker(
  	  string $markerId, string $name, float $latitude, float $longitude
  ): void {
    $name = new Name($name);
    $location = new Location($latitude, $longitude);

    $this->markers = $this->markers->add(
      new Marker($markerId, $name, $location, $addedAt)
    );
  
  	$this->viewport = new Viewport($this->markers);
  }
  
  // ...
}

Call a method instead of using several setters

Don't break

encapsulation with getters

Public method

Public method

Class

Public method

Constructor

Internal

State

Getters

findClosestMarker

getMarkers

final class Location
{
  public function __construct(
    private readonly float $latitude,
    private readonly float $longitude,
  ) {
    if (90 < $latitude || -90 > $latitude) {
      throw new \OutOfRangeException('The latitude must be between -90 and 90');
    }

    if (180 < $longitude || -180 > $longitude) {
      throw new \OutOfRangeException('The latitude must be between -180 and 180');
    }
  }
  
  public function equals(Location $location) {
  	return 
      $location->latitude === $this->latitude &&
      $location->longitude === $this->longitude
    ;
  }
}

Ensure business invariant

final class Map
{
  public function addMarker(
    string $markerId, string $name, float $latitude, float $longitude
  ): void {
    if ($this->markers->hasMarkerLocatedAt(new Location($latitude, $longitude)) {
      throw new \Exception(
        'Cannot add the marker, a marker is already located there'
      );
  	}

    $name = new Name($name);
    $location = new Location($latitude, $longitude);

    $this->markers = $this->markers->add(
      new Marker($markerId, $name, $location, $addedAt)
    );
    
    $this->viewport = new Viewport($this->markers);
  }
}

Ensure business invariant

final class MarkerList
{
  private array $markers = [];
  
  public function __construct(Marker ...$markers)
  {
    $this->markers = $markers;
  }

  public static function empty(): MarkerList
  {
    return new self();
  }

  public function add(Marker $marker): MarkerList
  {
    return new self($marker, ...$this->markers);
  }
  
  public function remove(string $markerId): MarkerList { /** ... */ }
  
  public function hasMarkerLocatedAt(Location $location): bool { /** ... */ }
}

Wrap properties logic into a Value object

Anemic models prevent you to know what happens !

You only manipulate data structures

You canot inform the rest of the application about what happens

Collect the aggregate events

addMarker

Get and dispatch

Map

records

MarkerAdded

final class MarkerAdded
{
  public function __construct(
    private readonly string $mapId,
    private readonly string $markerId,
    private readonly Name $name,
    private readonly Location $location,
  ) {}
}

final class Map
{
  public array $events = [];
  
  // ...
  
  public function addMarker(string $markerId, string $name, /** Etc. */): void
  {
    $name = new Name($name);
    $location = new Location($latitude, $longitude);

    $this->markers = $this->markers->add(
      new Marker($markerId, $name, $location, $addedAt)
    );
    
    $this->viewport = new Viewport($this->markers);

    $this->events[] = new MarkerAdded($this->mapId, $markerId, $name, $location);
  }
}

training : Hexagonal Architecture & Tactical DDD

Thank you!

Any Questions?

@arnolanglade

Feedback is always welcome

Agile Pays Basque (6-7sept)

On the Basque coast in Bidart

final class Map
{	
  public static function create(string $mapId, string $name): self {
  	//...
  }

  public function addMarker(string $markerId, string $name, /** etc. */): void {
  	//...
  }

  public function shareWithFriend(string $cartographerId): void {
  	//...
  }

  public function addAssetsToMapGallery(string $assetId, /** etc. */): void {
  	//...
  }
}

No more Getters and setters