- Published on
The ambiguity, the curse and the fallacy of domain model
- Authors
- Name
- Damian Płaza
- @raimeyuu
This tale referrs to topics presented in a great presentation given by Casey Muratori. I strongly advise watching it (even before reading this tale).
This blogpost is not an Entity Component System (ECS) tutorial, although we're going to try to exploit some of the ideas from ECS.
This blogpost will use pseudocode in a TypeScript (TS) like syntax - as TS does not support multiple inheritance, we still could use mixins to achieve similar reusability (and pains).
I am not doing game development professionally (I am gamedev noob!), so I might be wrong in some places when it comes to ECS and gamedev related parts.
How can we help?
Meet Tom (you might recall him from Many Faces of DDD Aggregates in F#).
As a successful businessman, after he earned tons of money with disco clubs, he decided to start creating games.

Hi, my name is Tom.
I have some money to invest in game development, as I heard is a very lucrative domain.
Can you help me with some ideas?
Game dev? Woah, that for sure will be a fun project! Let's help Tom!
Tell us more, Tom.

Hmm, seems too small for me, but hell - he's the boss, isn't it?
Time for designing the game. We know that there is a concept of a player and a robot, so let's start with that.
class Player {
constructor(public name: string) {}
}
Even though Tom didn't mention it, we know that the player will have a name, so let's add it.
Everyone has a name, right?
Now a robot.
class Robot {
constructor(public type: string) {}
}
It's not just a robot, it's a specific type of robot, so let's add a type to it.
Yeah, Tom didn't mention it, but we know how those business guys think - for sure they will want to add more types of robots in the future.
Both entities should be able to move (if you listed carefully, dear Reader), so let's fix that.
class Player {
constructor(public name: string, private position_x: number, private position_y: number) {}
moveLeft() { // 👈 new code!
this.position_x -= 2;
}
moveRight() {
this.position_x += 2;
}
moveUp() {
this.position_y += 2;
}
moveDown() {
this.position_y -= 2;
}
}
class Robot {
constructor(public type: string, private position_x: number, private position_y: number) {}
moveLeft() { // 👈 new code!
this.position_x -= 1;
}
moveRight() {
this.position_x += 1;
}
moveUp() {
this.position_y += 1;
}
moveDown() {
this.position_y -= 1;
}
}
Great, both entities can move now. The player can move faster than the robot.
This should make it a little easier for the player to survive and get some points.
Start small, but grow big?
Wait a second, we forgot something important.
Even though Tom mentioned we are going to start small, we know how this works - sooner or later there will be more enemies.
Let's do some refactoring to prepare for that.
abstract class MovableEntity { // 👈 new code!
constructor(private position_x: number, private position_y: number) {}
moveLeft() {
this.position_x -= this.movementSpeed;
}
moveRight() {
this.position_x += this.movementSpeed;
}
moveUp() {
this.position_y += this.movementSpeed;
}
moveDown() {
this.position_y -= this.movementSpeed;
}
abstract get movementSpeed(): number;
}
class Player extends MovableEntity {
constructor(public name: string, position_x: number, position_y: number) {
super(position_x, position_y);
}
get movementSpeed() { // 👈 to make the player faster than the robot
return 2;
}
}
class Robot extends MovableEntity {
constructor(public type: string, position_x: number, position_y: number) {
super(position_x, position_y);
}
get movementSpeed() { // 👈 to make the robot slower than the player
return 1;
}
}
There should probably be a base class for all entities that can move, so we created MovableEntity
.
It also aligns with our understanding of the domain - exactly how Tom described it.
Of course Tom is not a technical person and he didn't say so but we know that both entites should be renderable!
abstract class RenderableEntity { // 👈 new code!
constructor(private position_x: number, private position_y: number) {}
abstract render(): void; // 👈 new code
}
class Player extends MovableEntity, RenderableEntity { // ⚠️ we simulate that TS supports multiple inheritance
constructor(public name: string, position_x: number, position_y: number, private context: CanvasRenderingContext2D) {
super(position_x, position_y);
}
// existing code...
render() { // 👈 new code
context.fillStyle = "blue";
context.fillRect(position_x, position_y, 100, 100);
}
}
class Robot extends MovableEntity, RenderableEntity { // ⚠️ we simulate that TS supports multiple inheritance
constructor(public type: string, position_x: number, position_y: number, private context: CanvasRenderingContext2D) {
super(position_x, position_y);
}
// existing code...
render() { // 👈 new code
context.fillStyle = "red";
context.fillRect(position_x, position_y, 100, 100);
}
}
Tom for sure will be happy to see that we are thinking ahead and preparing for future requirements.
The game loop will be responsible for rendering the entities. We skip it for brevity.
Let's check our architecture so far:

Makes sense - Player
entity is a MovableEntity
and is a RenderableEntity
, and the same goes for Robot
.
Let's see what Tom thinks about it.

Yeah, the game is amazing, even though it looks so simple.
As we are Agile, I asked some gamers to play it and got some feedback. They said that it's pretty boring that we are just running away and robot is so naive.
Boring game won't sell
Well, Tom is right, the game is boring.
Let's add some attacking logic to the robot.
Of course, sooner or later the player will be able to attack the robot too, so let's prepare for that!
Hehe, that's a smart move.
abstract class AttackingEntity { // 👈 new code
constructor(private position_x: number, private position_y: number) {}
abstract attack(entity: EntityWithHealth): void; // 👈 we will define it in the next section
abstract get attackPower(): number;
}
class Robot extends MovableEntity, RenderableEntity, AttackingEntity { // ⚠️ we simulate that TS supports multiple inheritance
constructor(public type: string, position_x: number, position_y: number, private context: CanvasRenderingContext2D) {
super(position_x, position_y);
}
// existing code...
attack(entity: EntityWithHealth): void {
entity.takeDamage(this.attackPower);
}
get attackPower(): number {
return 1;
}
}
Ok, now Robot
can attack Player
- it is represented by EntityWithHealth
, because we know that a Player
is a EntityWithHealth
.
Probably this will pay off when the player will be able to attack the robot too.
Let's define EntityWithHealth
:
abstract class EntityWithHealth { // 👈 new code
constructor(private health: number) {}
takeDamage(damage: number): void {
this.health -= damage;
if (this.health <= 0) {
this.die();
}
}
}
class Player extends MovableEntity, RenderableEntity, EntityWithHealth { // ⚠️ we simulate that TS supports multiple inheritance
constructor(public name: string, position_x: number, position_y: number, private context: CanvasRenderingContext2D) {
super(position_x, position_y);
super(100); // 👈 let's give the player 100 health points
}
}
Now the player can take damage and die (this will make the game stop).
Let's review the architecture again:

Tom is happy with the changes, but he has one more request.

More robots, more fun
Who's the best software architect in the world?
As anticipated, more types of robots are coming.
Great that we did some speculative designing and prepared for that.
Let's do some changes. But remember - we want to keep the code DRY and avoid code duplication.
We should probably create a base class for all robots.
abstract class Robot extends MovableEntity, RenderableEntity, AttackingEntity { // 👈 new code
constructor(public type: string, position_x: number, position_y: number, private context: CanvasRenderingContext2D) {
super(position_x, position_y);
}
}
class SlowRobot extends Robot { // 👈 new code
constructor(type: string, position_x: number, position_y: number, context: CanvasRenderingContext2D) {
super(type, position_x, position_y, context);
}
get movementSpeed() {
return 1;
}
get attackPower() {
return 1;
}
}
class FastRobot extends Robot { // 👈 new code
constructor(type: string, position_x: number, position_y: number, context: CanvasRenderingContext2D) {
super(type, position_x, position_y, context);
}
get movementSpeed() {
return 2;
}
get attackPower() {
return 1;
}
}
We will make Robot
an abstract class, so it cannot be instantiated directly.
Also, let's make slower robot a bit stronger in attack power.
Faster robot will be able to move faster, but it will be weaker in attack power.
This should make the game more interesting.
Time to review the architecture again:

Let's ask Tom about the feedback.
We need more real-life behaviors!

A-M-A-Z-I-N-G ideas!
Good that we have well-defined classes which clear responsibilities.
Robot
has a fuel
and it can deplete when moving.
Let's model it accordingly.
abstract class Robot extends MovableEntity, RenderableEntity, AttackingEntity {
constructor(public type: string, position_x: number, position_y: number, private context: CanvasRenderingContext2D) {
super(position_x, position_y);
this.fuel = 100; // 👈 let's give the robot 100 fuel points
}
private fuel: number;
moveLeft() {
if (this.fuel > 0) {
super.moveLeft();
this.fuel -= 1;
}
}
moveRight() {
if (this.fuel > 0) {
super.moveRight();
this.fuel -= 1;
}
}
moveUp() {
if (this.fuel > 0) {
super.moveUp();
this.fuel -= 1;
}
}
moveDown() {
if (this.fuel > 0) {
super.moveDown();
this.fuel -= 1;
}
}
attack(entity: EntityWithHealth): void {
if (this.fuel > 0) {
super.attack(entity);
}
}
get movementSpeed() {
return this.fuel > 0 ? this._movementSpeed : 0; // 👈 no movement when out of fuel
}
}
I am glad we picked class-oriented design in this game.
It allows us to easily add new behaviors to the robots without changing the existing code.
This time the architecture remained the same.
Truly magnificent design!
Game is too hard, let's make it a bit more entertaining

Hm, that's very intersting. Robot
might also get a power-up that will restore its fuel - some time in the future.
We need to add a PowerUp
class that will need to update health of the player, that's easy.
But when an attacking robot attacks the player, it will get its fuel depleted - where should we put this logic?
Where should we modify the robot's fuel?
Should it happen in a EntityWithHealth
class? No, it should not.
Then the Robot
would know that EntityWithHealth
has a PowerUp
.
Maybe we should add a new class EntityWithPowerUp
, that EntityWithHealth
will inherit from?
Strange, it was supposed to get easier, not harder, when doing class-oriented design...
Retrospection!
What do you think about our design, dear Reader?
Of course, I was exaggerating all the time as people don't use inheritance in the real world, right?
So far so good, one might say.
Do you see where it all goes?
Even if it is a simple game, we already created quite a nice entanglement of classes.
"Damian, all the people in the industry know that inheritance is bad, so why are you writing this blogpost?", one might say.
"Favor composition over inheritance, Damian!", some might urge me, seeing all this mess I've made.
Let's repeat it together:
Favor objects composition over class inheritance
Yes, it totally makes sense! (I am not an LLM, I am not confirming it just because you thought so, dear Reader!)
Should we try to use objects composition instead of class inheritance and see how it goes?
Round 2: composition over inheritance
Let's start with the same requirements and progress slowly.
So initially we have a Player
and a Robot
.
interface ICanMove {
moveLeft(): void;
moveRight(): void;
moveUp(): void;
moveDown(): void;
}
class Player implements ICanMove {
constructor(public name: string, private position: Position) {}
// all the methods from `ICanMove` interface
}
class Robot implements ICanMove {
constructor(public type: string, private position: Position) {}
// all the methods from `ICanMove` interface
}
Pretty easy and straightforward.
Let's add rendering capabilities.
interface ICanBeRendered {
render(context: CanvasRenderingContext2D): void;
}
class Player implements ICanMove, ICanBeRendered {
constructor(public name: string, private position: Position, private canvas: CanvasRenderingContext2D) {}
// all the methods from `ICanMove` interface
// all the methods from `ICanBeRendered` interface
}
class Robot implements ICanMove, ICanBeRendered {
constructor(public type: string, private position: Position, private canvas: CanvasRenderingContext2D) {}
// all the methods from `ICanMove` interface
// all the methods from `ICanBeRendered` interface
}
We are getting on speed.
As long as our entities are not having interactions, it seems fine.
Of course the game is checking if they are not colliding, but we can handle it in a separate class.
What was next? Ah, attacking and health.
interface ITakeDamage {
takeDamage(damage: number): void;
}
class Player implements ICanMove, ICanBeRendered, ITakeDamage {
constructor(
public name: string,
private position: Position,
private canvas: CanvasRenderingContext2D,
private health: Health
) {}
// all the methods from `ICanMove` interface
// all the methods from `ICanBeRendered` interface
// all the methods from `ITakeDamage` interface
}
interface IAttack {
attack(entity: ITakeDamage): void;
}
class Robot implements ICanMove, ICanBeRendered, IAttack {
constructor(
public type: string,
private position: Position,
private canvas: CanvasRenderingContext2D,
private attack: Attack
) {}
// all the methods from `ICanMove` interface
// all the methods from `ICanBeRendered` interface
// all the methods from `IAttack` interface
}
We don't even need to show the architecture, as it is pretty straightforward - there's is a pure composition of interfaces.
But wait a second - what does the composition mean in this case?
Objects compose other objects and expose capabilities through well-defined interfaces.
Now adding a new types of robots can be done easily without changing the existing code, as we don't play "inheritance game" (pun intended).
We learned by the hard way that inheritance leads to explosion of complexity, quite soon. (I hope you were able to simulate the pain of inheritance and painful evolution of inheritance structures, dear Reader).
Should we try to satisfy Tom's requirements related to fuel and power-ups?
class Robot implements ICanMove, ICanBeRendered, IAttack {
constructor(
public type: string,
private position: Position,
private canvas: CanvasRenderingContext2D,
private attack: Attack,
private fuel: Fuel // 👈 robot aggregates fuel so that we can deplete it
) {}
// all the methods from `ICanMove` interface
// all the methods from `ICanBeRendered` interface
// all the methods from `IAttack` interface
moveLeft() {
if (this.fuel.isDepleted()) { // 👈 here we check if there's any fuel available
return;
}
this.position.moveLeft();
this.fuel.deplete(); // 👈 we deplete the fuel when moving
}
// other movement methods...
}
Magic - we don't need to change any interface, hence we don't break the contract!
It's clear that Robot
has a Fuel
, and we model it accordingly.
The power of composition, yay!
Now we can add a PowerUp
concept that will be able to restore health of the player or de-fuel of the attacking robot, when player has this power-up.
interface IHaveFuelDepletingPowerUp {
usePowerUp(callback: () => void): void; // 👈 we use a callback to perform the action
}
class Player implements ICanMove, ICanBeRendered, ITakeDamage, IHaveFuelDepletingPowerUp { // 👈 player aggregates power-ups
constructor(
public name: string,
private position: Position,
private canvas: CanvasRenderingContext2D,
private health: Health,
private powerUp: PowerUp // 👈 player aggregates power-up
) {}
// all the methods from `ICanMove` interface
// all the methods from `ICanBeRendered` interface
usePowerUp(callback: () => void): void {
if (this.fuelDepletingPowerUp.isAvailable()) {
this.fuelDepletingPowerUp.use(callback); // 👈
}
}
}
class Robot implements ICanMove, ICanBeRendered, IAttack {
constructor(
public type: string,
private position: Position,
private canvas: CanvasRenderingContext2D,
private attack: Attack,
private fuel: Fuel
) {}
// all the methods from `ICanMove` interface
// all the methods from `ICanBeRendered` interface
// all the methods from `IAttack` interface
attack(entity: ITakeDamage & IHaveFuelDepletingPowerUp): void { // ⚠️ we needed to change the contract!
if (this.fuel.isDepleted()) {
return;
}
entity.usePowerUp(() => this.fuel.deplete()); // 👈 we use the power-up if available
entity.takeDamage(this.attack.power);
this.fuel.deplete(); // 👈 we deplete the fuel when attacking
}
}
It wasn't that difficult, was it?
Unfortunately, we broke the contract of IAttack
interface, so we would need to update all places where this interface is used.
But let's make a side-quest and think about the future requirements - what if there will be an obstacle that could be destroyed by the robot?
In a way, Robot
would be able to "attack" the obstacle, and the obstacle would be able to "take damage".
Then this method of IAttack
:
// we are using union (&) of two interfaces so this entity provides both capabilities
attack(entity: ITakeDamage & IHaveFuelDepletingPowerUp): void;
would be a perfect fit for the Obstacle
class, wouldn't it?
Of course, obstacles do not have power-ups, but it's not a problem as we could use Null Object pattern:
class NullPowerUp implements IHaveFuelDepletingPowerUp {
usePowerUp(callback: () => void): void {
// do nothing
}
}
Obstacle would implement this interface and use NullPowerUp
as a power-up, so it would not break the contract.
But imagine such a requirement - a player is able to cast a spell to make objects (not in the software sense, but "obstacle" sense) indestructible for a while.
We could compose it further and make an Obstacle
object use more PowerUp
s that would allow it to become indestructible.
Retrospection!
What do you think about our design, dear Reader?
Of course, we are playing with the design and mental models (ways of thinking) here, so there might be better ways to model this little reality.
We could compare it to class-oriented design that we used earlier.
In the inheritance-based round, we had a huge and continuously growing hierarchy of classes.
Locating responsibilities, navigating through the maze of intertwined classes, understanding what mutates what - horror.
In the objects-composition round, we have a flat structure of interfaces that are composed together to model entities.
Capabilities provided by each entitiy are clearly defined and exposed.
Interactions between entities happen but because we have keeping the structure as flat as possible, it's easier to follow up on individual entities.
Also, "the power of composition" allows us to easily add new behaviors without changing the existing code.
In case of extending the entities without modifying them directly, we could use Decorator pattern to add new behaviors, switch behaviors in runtime using Strategy pattern, and so on.
But let's come back to the presentation given by Casey Muratori.
Casey in his presentation brought very important concept: compile-time hierarchies.
Many people immediately associate hierarchies with inheritance, which of course is true - we literally saw them growing in the inheritance-based round.
Casey did enormous effort to do the research of the origins of making "the mistake" - which isn't the object-orientation itself, but creating those compile-time hierarchies.
One (it is a quote) of his slides states the following:
A compile-time hierarchy that matches the domain model.
In the object-composition round this is exactly what we are doing - we are creating "a well-defined domain model" - where Player
, Robot
and other concepts are clearly expressed.
We even can talk with Tom and exchange ideas without "solutioning" that "Player is a Movable entity", for example.
The compile-time structure is pretty flat (in comparison to inheritance-based design), but the domain model classes grow in size as we add more capabilities to them.
Methods are getting longer, more ifs are happening here and there.
We basically created a "God Class" - a compile-time, absolute-based, reality-chasing models.
We tried to model the reality and express it in the code, but whoever tries to capture the reality in compile-time, will fail in re-design time and in the runtime later.
One, big lesson from the presentation, that one can take away is boundaries (encapsulation) are one of the most important aspects when designing software
We should train ourselves in looking for boundaries reflecting the domain, we are trying to build software solutions for, but not materialize them as compile-time hierarchies.
Should we try to use Entity Component System (ECS) and try to model the domain of this game?
Round 3: Entity Component System (ECS)
Let's quickly recap what Entity Component System (ECS) is.
ECS is a software architectural pattern that is used to model complex systems, especially in game development.
Entities are concepts that are represented in the world: like Player
, Robot
, Obstacle
, etc.
They are just identifiers.
Nothing more, nothing less.
Each of the concepts have related components - Position
, Speed
, Health
, Fuel
, Attack
, etc.
These components are just data that represent a very specific perspective of the entity's state.
Finally, systems are responsible for transforming relevant components of the entities, based on the specific use case, through time.
Let's take a system representing Movement
that will take components: Position
and Speed
, to update the entity's location based on its velocity.
We could have a system representing MechanicalMovement
that will take components: Position
, Speed
, and Fuel
, to update the entity's location based on its velocity, adjusting fuel consumption based on the speed and distance traveled.
We can compose components in systems to create complex behaviors.
This means we are not creating compile-time hierarchies, nor the domain model which would play a weak representation of the reality.
Previously, we didn't mention it directly, but one of the "assumed" traits that should be given by class-oriented design was encapsulation.
Of course, we still need to modify code, adjust components, introduce systems (and evolve them), but boundaries are established differently.
But they are there.

The red lines are "encapsulation boundaries". They are organized around components, not around entities.
And here's Casey's comment to the slide shown above:
"Encapsulation boundaries are really what we care about in software design. What we care about is where we make it difficult to access things and where we make it easy, right?"
"What you hide, you can change", and even though components are pure data structures in game development, ECS architecture allows composing requirements at runtime.
This flexibility is crucial for adapting game mechanics and behaviors dynamically.
Imagine that we finally want to add attacking capabilities to the player.
We would need to wire up Player
entity (an identifier) with Attack
component and existing system should be able to handle that seamlessly (or at least without big hiccups).
Ok, let's jump to DDD world for a while.
Domain-Driven Design (DDD) and Entity Component System (ECS)
I hear you dear Reader - "Damian, why the hell are you talking about DDD and ECS in the same tale?".
And that is a very good question!
In DDD we care a lot about the domain model, which tries to represent the areas of the business we are building software for.
Ubiquitous language, bounded contexts, domains, subdomains and so on - everything about tackling the complexity and managing it through time.
One can often see tutorials showing how to "model a domain" with so-called "tactical patterns" like aggregates, entities, value objects, and so on.
And we end up with concepts of "Users", "Products", "Tours", "Posts", and so on.
Should we take an example, dear Reader?
Are you interested in a tour?
You remember Tom, right?
He again approached us with a new idea.

It is the first time we hear about this idea, but it sounds like a great business opportunity.
We should run Event Storming session with Tom to understand the domain better, but let's try to model the domain with DDD approach and quickly verify our understanding with Tom.
Ok, let's recap: a Tour
has a TourName
, TourDescription
, BasePrice
, TourStartDate
, and AvailableSeats
.
Also, a Tour
has Participants
, and each Participant
has a Session
.
And not forget that a Tour
has a Guide
. Probably Guide
has Name
, Experience
, and Rating
.
class Tour {
constructor(
private name: TourName,
private description: TourDescription,
private basePrice: BasePrice,
private startDate: TourStartDate,
private seats: AvailableSeats,
private participants: Participants
) {}
reserveSeats(reservation: SeatReservation): void {
if(!reservation.isPaid) {
DomainError.raise("Reservation is not paid");
}
if(this.seats.canReserve(reservation.participants.length)) {
this.seats.reserve(reservation.participants.map(toSeat));
this.participants.add(reservation.participants);
} else {
DomainError.raise("Not enough available seats");
}
}
getAvailableSeats(): AvailableSeats {
return this.seats.toAvailableSeats();
}
allocateGuide(guide: Guide): void {
this.guide = guide;
}
getGuide(): Guide {
return this.guide;
}
updateName(newName: TourName): void {
this.name = newName;
}
updateDescription(newDescription: TourDescription): void {
this.description = newDescription;
}
// and more, and more...
}
Ok, initial round of modeling is done. In DDD it is very, very important to collaborate with domain experts, so let's ask Tom about our understanding.
As you see dear Reader, we used Value Objects and Entities to express important concepts of the domain.
(here is the moment when we show the code to Tom)

Wow, great that we had this conversation.
We forgot to add Status
to Tour
class - how the hell could we forget about it?!
Let's adjust the code accordingly.
class Tour {
// existing code...
private status: TourStatus = TourStatus.Planned; // 👈 initial status for a tour.
start(): void {
if (this.status !== TourStatus.Planned) {
DomainError.raise("Tour can only be started when it is planned");
}
this.status = TourStatus.InProgress;
this.participants.startSessions();
this.guide.startSession();
}
end(): void {
if (this.status !== TourStatus.InProgress) {
DomainError.raise("Tour can only be ended when it is in progress");
}
this.status = TourStatus.Completed;
this.participants.endSessions();
this.guide.endSession();
}
getStatus(): TourStatus {
return this.status;
}
allocateGuide(guide: Guide): void { // 👈 we need to check if the guide is available
if (this.status !== TourStatus.Planned) {
DomainError.raise("Tour can only have a guide allocated when it is planned");
}
if(guide.isAvailable(this.startDate)) {
this.guide = guide;
} else {
DomainError.raise("Guide is not available for the selected date");
}
this.guide = guide;
}
}
Phew, that's better.
What a nice domain model we have here.
It is clear and expressive, even Tom was able to read it.
This must be the holy grail of Domain-Driven Design!
Compile-time domain model disaster
Let's take a while and admire domain modeling we just saw.
So DDD.
We again fall into the compile-time, absolute-based, and reality-chasing trap.
We hear Tour
and we immediately think about Tour
class.
Even though it nicely expresses the language of the domain, it is still a compile-time hierarchy.
Congratulations, we just established a "God Class".
Such Noun-based, reality-chasing model attracts so many responsibilities, so many use cases, that it will soon become a nightmare to maintain (and evolve).
Let's slow down, take a deep breath and think about various components of the Tour
concept.
What are important systems, or use cases in this particular business?
Tour booking?
Tour navigating?
Tour name updating?
Allocating guide to a tour?
In each of those scenarios, we will only need some specific aspects of the tour.
We could use one of many heuristics for "finding components" and establishing good boundaries - like anti-requirements.
Let's ask ourselves a question: how probable is it that we would need to check the Tour's name while allocating a guide?
Not very probable, right?
What about checking if seats are available while we are navigating the tour and we are tracking each participant's position?
Not very probable either.
While exploring the results of applying anti-requirements heuristic, we found out that when someone would try to allocate guide to a Tour and change the name of the Tour at the same time, then one of the operations will probably fail - or at least introduce an inconsistency in our system.
It seems that we can segregate some components that might be useful only in some specific use cases:
TourDetails
SeatsAvailability
TourNavigationSessions
GuidesAvailability
- etc.
(note dear Reader it is just a suggestion, not a final list of components).
Imagine that there's a system (something like "a business process") that will be triggered when all reservations are paid.
Let's call it TourNavigationPreparation
system - we could also call it a TourNavigationPreparationProcess
, if you wish, dear Reader.
Such system will need to:
- try allocating
Guide
component to theTour
component - prepare
TourNavigationSessions
components for eachParticipant
component and for theGuide
component - prepare
TourNavigating
component ensuring the list of Checkpoints components is available
This is just one of many systems that could be used to prepare a tour for navigation.
Can you tell where does it lead us, dear Reader?
Where is "Tour" entity?
TourNavigationPreparation
system is a good example of a business process that might be quite complex.
It organizes various components (or capabilities, if you wish) into a meaningful sequence of steps.
Imagine if a PlannedTour
required transporting participants to the starting point of the tour - in another city.
This means that we need to add a ParticipantsTransport
component that will be used in the TourNavigationPreparation
system.
Such step is relevant only in this particular use case, in a very specific moment of time.
"Ok, but where is our Tour, Damian?", one might ask.
This is really funny.
Everywhere and nowhere.
There is no central place where Tour
can be found.
It is composed in "the runtime", throughout each process.
What is the most important is its identifier - TourId
.
So in a way, there is no domain model.
No single one.
But there are various domain models - a model of seat availability, a model of tour details, a model of location tracking, a model of guide availability, and so on.
Alone, they might not be that powerful, until you compose them (their capabilities) and "a new capability will emerge" - a system.
A system is greater than a sum of it components.
Did you say "encapsulation"?
Game development is unique domain as the performance is one of the biggest concerns one must take care into account, from day zero (at least that's how I imagine it).
Components in ECS are pure data structures, striving to be stored in a specific memory layout, so that when manipulated by systems it's super efficient to bulk update data inside of the component boundary.
And that is one of possible reasons why in gamedev one wants to "encapsulate" components in such a way.
But because they are pure data structures, it's a "different encapsualtion" than one might be typically think of - each system can manipulate components to achieve its designed purpose (for example: changing position component of a mechanical entities and adjust fuel component).
In object-oriented design (and not only), one might think of the encapsulation as a way of hiding the implementation details (e.g. how we calculate taxes, what data structures are used, etc.) so that it's easier to deal with the changes later.
Let's hide "implementation details" of previously shown slide from Casey's presentation and put them inside of bubbles:

Finally, arrows, boxes and circles - serious architecture diagram!
Let's add some exemplary systems:

Let's imagine that we want to introduce some kind of "special events" that will change the gameplay in runtime.
Something like "a day of tireless mechs" - you get the point dear Reader.
In such "a day", the fuel component is not depleting when any action is taken by any entities having a fuel component.
Where should such responsibility be assigned?
Ok, now it's time to a blasphemy - I think we are missing a concept.
We could name it as "a policy system" - kind of a "higher level" system that is able to fine-tune "lower level" systems.
For brevity, let's call such systems policies.
So "a day of tireless mechs" is a policy, let's express it within a particular boundary - "Game events" policy:

Of course, take it with a pinch of salt, dear Reader.
We are playing with ideas here, bring metaphors and try looking for patterns, analogies and synergies.
Did anyone say "levels"?
I am not sure about you, dear Reader, but it seems to me that we have some kind of a hierarchy, don't you think?

It looks familiar.
Of course, components in gamedev are pure data structures, systems are processing units that manipulate these components to achieve desired behaviors and outcomes.
Policies we have just introduced (as a concept), but even if they are not existing exactly in such a form, conceptually there might be something that "plays" the role of a fine-turning actor.
How would that look in Domain-Driven Design?
Thankfully, there's a concept called "Large-scale structure".
The referred blog post presents very interesting diagram:

As we can see, there are various "responsibility layers" with stated change rate or "volatility". Each layer expresses certain "knowledge boundary" that describes how another "knowledge boundary" should behave.
One could say that this structure represents a hierarchy of "knowledge boundaries" that are organized around the domain.
Operations level tells Potentials what should be achieved, in what sequence.
Policies level tells Operations what are the rules that should be used, and so on, and so on.
Can you see an analogy to components, systems and policies in the context of Entity Component System (ECS)?
Of course, application of ECS is slightly different - as we described earlier with analyzing the meaning of "encapsulation" in the context of ECS.
But in principle, we want to organize the knowledge (and responsibilities), and establish boundaries differently, to achieve certain goals.
Tourism and knowledge levels
Now let's put gamedev aside and think of applying the explored knowledge (pun intended) to TourNavigationPreparation
process, from Compile-time domain model disaster section.
As a recap, here is what such a process is intended to do:
- try allocating
Guide
component to theTour
component - prepare
TourNavigationSessions
components for eachParticipant
component and for theGuide
component - prepare
TourNavigating
component ensuring the list of Checkpoints components is available

As we are not in gamedev anymore, we want to each component protect its invariants (think: rules) and do not let their state be corrupted (think: inconsistent).
This means that whenever we try to "allocate a Guide
to a Tour
", when there's a Guide
that is not available, it won't let us to do that.
Such rules will be placed within a boundary of each component and will be enforced by the component itself (remember: we are not in gamedev realm anymore, we are playing with analogies here.)
One could say that we encapsulate those rules and information required to satisfy those rules inside a boundary of a specific component.
What if there will be a special fine-tuning required? We would probably introduce a "TourNavigationPreparationPolicy" policy that would be able to adjust the process, based on some conditions.
Also, it's worth noting that components presented in the diagram should strive to be used in a specific processes or use cases.
"Damian, what about coupling?", you might ask, dear Reader.
And this is a very good question, indeed!
Coupling, boundaries and encapsulation
Complexity does not disappear. (due to interactions between or elements/components - if you're interested about reading more, please check Composing a PIE)
We are mindful engineers and we know that "loose coupling" is just a buzzword, as coupling does not disappear, but moves and changes places.
As we explored "Large-scale structure", some knowledge levels are more volatile, and some are more stable.
It might be that our business people want to experiment with different ways of organizing a tour navigation preparation process - based on the fact that we're dealing with corporate or individual customers, if there are any special events (free meal during the day, etc.).
This means that this "knowledge level" might be a target of more frequent changes.
What about "tracking location", "guide availability", "participant details", "seats availability", and so on?
Of course it might still vary, but it seems that those concepts might be more stable, when it comes to rules and changing them.
In our tourism example, we encapsulated and separated what varies and what seems to be more stable (according to our understanding of the domain).
Our little TourNavigationPreparation
process, which seems to be more volatile, depends on more stable components.
We could say that we intentionally designed their interactions in such a way so that we got something back - for instance, stable components remain stable.
Also, "guide availability" and "tracking location" capabilities does not know why they are used, they just provide their capabilities.
"Higher level" operations know the reasons - as in our little example with TourNavigationPreparation
process.
Let's take "tracking location" - it might be used to track real people location - guides, participants - but also for "simulated zombies" that are chasing our participants.
We could add a new process that would be able to introduce "geo-gaming" while participanting in a tour.
This is yet another nice result we got back from organizing knowledge boundaries in such a way.
The complexity of the process got "reduced" (in fact, moved) and encapsulated in underlying capability providers.
And that's the power of tackling the complexity in its heart.
There's yet another perspective - which is related to software development, teams and their interactions.
We want to organize our bounded contexts in such a way that people working on them could work as independently as possible (which does not mean in a complete isolation!).
Strategic Domain-Driven Design brings more "tools" when it comes to analyzing types of domains and by having those information - organizing boundaries in a certain way.
And here comes the Context Mapping activity - it might give us the details of the impact of changes, dependencies, and so on.
From domain model to domain models
The idea of "a domain model" isn't bad - hell, it's crucial to manage the complexity!
But the idea of "the domain model" is a fallacy as it brings "God classes", compile-time hierarchies, and absolute-based thinking.
This is the curse of not deepending ones understanding enough, and trying to capture the reality in a single model.
The intentions are great, but the results can bring mess and a lot of pain, as "domain model" is so ambiguious term.
We should strive to create smaller domain models - each tackling a very perspective that might appear in a business domain we are create software solutions for.
Each of those domain models can have various depth of implementation, depending on needs we identify.
Domain models might be used in different moments of time, depending on the process or use case we are trying to cover.
As you saw, dear Reader, "hierarchies" did not disappear, but they got transformed and moved onto a different level.
Instead of "compile-time hierarchies", one could say we got "runtime hierarchies", that can be composed at our will, in a more "dynamic" way.
Of course, we might introduce changes that will cause a ripple effect and make it difficult to accomodate them easily - that's inevitable aspect of software engineering and software design.
Complexity does not disappear, it just gets moved elswhere.
It was a long journey, dear Reader, but I hope you enjoyed it.
We explored compile-time hierarchies that can appear both in gamedev and business systems.
We tried to apply "Entity Component System" thinking to tackle the complexity of business problems we are all experiencing in a day to day work.
In business applications, components are not "pure data structures" as in gamedev, as they encapsulate capabilities and rules that are related to those capabilities.
Business applications can have systems that are stateful, tracking the progress of underlying processes or workflows.
Even though gamedev and business applications might seem different in terms of problems, I feel that conceptually they are using similar approaches for dealing with complexity: "what you hide, you can change".
Next time dear Reader, when you hear about "domain model", think about "domain models" instead.
Domain models that within their boundaries are able to encapsulate capabilities and rules, and can be composed together to let more complex behaviors emerge.
See you next time!