Entities

The veritable gilded halls of DDD which, like the best songs in a concert, come in after quite a bit of build-up: This is the "Master of Puppets" of DDD patterns... "Master of Patterns"? Dad joke.
Illustration from Undraw
TL;DR
Entities are maybe the reason you learned about DDD in the first place. At their heart, the Entity is concerned first and foremost about the virtues of conventional OOP and SOLID, and not accepting passing dumb data containers around.
Every Entity has a unique identity. We use Entities to wedge in domain logic on "things" rather than abstract "SomethingServices" and other techno-speak that divorces the domain from the coded implementation. By operating on these things (Entities) with clear business/domain logic we solve a lot of poor programming practices.
Entities and Aggregates are practically the same, with the difference being that an Entity is a thing while an Aggregate represents a cluster of things.
Entities reside in the Domain layer.
Entities and Aggregates are perhaps the most "prominent" of the tactical patterns. It's important to understand that the notion of Entities in database-adjacent contexts and in implementation-oriented tools like Entity Framework are not the same thing.
Beware of snake oil salesmen
DDD has nothing to do with persistence technologies or databases. In fact, when taking a DDD approach and combining it with your required persistence tech, you'll most likely see that there are no shortcuts—you'll have to do the modeling and so forth on your own. Tools that "sell" how they map to DDD, like Entity Framework and some Object Relational Mappers, will not help you in any meaningful way.
Both of these concepts are very much related, and it probably makes sense to start with the more general of them: The Entity.
Entities are objects that may mutate (change) over time, and who all have distinct identities. We can think of a BookClubMember as something that feels quite right being an Entity as it implies a person and identity behind it. On the other hand, a Meeting may be a simple Value Object (more on these later), as it has neither a unique identity nor will it change after the fact. All in all, it's easy to see how a BookClubMember will be a much less simple construct than the Meeting.
Our example BookClubMember will most likely involve both data (such as identity, books read, membership date) and behavior (such as updating the member's address). It will also contain its own clear business rules attached to such behaviors, where a prospective rule could be something like renewing membership only after having paid the member's fee.
Entities are persisted (saved, loaded) with a Repository in the shape of a Data Transfer Object. Before using them in your code, you "turn them into" DTOs or into Entities. DTOs must never be directly mutated.
Let's make it all ultra clear: An Entity is an object. Most often we represent these as classes. Because a class can contain data we can logically manipulate that data. The way we manipulate the data is through methods on the Entity class that corresponds to our common (ubiquitous) language; We don't let anyone directly manipulate the data on the Entity instance. We can save a representation of the Entity's data (state) with a Repository and we can load back the data and reconstitute it into a valid Entity instance when needed. All of that would happen in the same Bounded Context, in our case, in the same solution (in turn consisting of Lambda functions).

Splitting data and behavior leads to unmaintainable code

In the world of traditional back-end engineering, you might find something like the below diagram: A service that interacts with several data sources. Because all of these are distinct and separated we have no good idea of who owns and may change, what source. At the bottom we have the faint contours of other services, too.
It's not uncommon that we for example:
  • Go with a data synchronization approach, where we duplicate data on our end (this may even be two-way but let's skip that idea for now). While it's easier today in the public cloud to set these things up, many times you'll still face the consequences of having to deal with data decoupled from the business logic (behavior). Further, you may get a problematic mix of eventual and strong consistency which could break transactional flows.
  • Decide to only read back data, making integration easier, but the resiliency and performance worse. An issue here is that at any point where a feature is needed that can update data, you will have a hard time getting that solution to be scalable, secure, and logical as the landscape is now polluted with multiple writers of the same leading data.
Either case will be poor in different ways.
Conceptual diagram of tangled integrations where separation of data and behavior leads to uncertainty of who can mutate data in which ways.
While in theory we have decoupling here, in essence, we also have created an even bigger problem: An anemic domain model.

The "anemic domain model"

The anemic domain model is one that represents objects as shells, or husks, of their true capabilities. They will often be CRUDdy as they allow for direct mutations through public getters and setters. It can quickly become hard to understand all the places in a codebase in which the data is manipulated, and how it was done.
Good code does more than just compile
Some find the criticism around "anemic domain models" academic and roundly wrong. They might argue that their experiences are that it's just as easy to get things to work, but with less hassle than going full-on with OOP.
Remember Robert Martin's words from Clean Code: A Handbook of Agile Software Craftsmanship: "It is not enough for code to work."
Speaking personally, for me code quality and structure are paramount when building something or when I work with (or coach) other engineers.
All this is measurable, once you have access to the code and not just raving to some rando on an internet forum. Using competent tooling you will likely get recommendations to fix issues like this, too. Good OOP and refactoring practices are practically institutionalized so this not "just a DDD thing".
The anemic type of objects will maybe do the job, but they will become liabilities too. They do not shield the objects from misuse, nor do they express the common language as succinctly.
The opposite of all of this, no surprise, is the "rich domain model"—really no more than a few opinionated ideas on top of your classic object-oriented programming. While that may not technically be the full truth, in our abbreviated version of DDD and the universe, then that explanation is good enough.

Rich domain models

The rich domain model is how Entities solve the question of "who" can do "what" on a specific dataset.
Compared to their anemic brethren, rich domain models (typically Entities and Aggregates) will be easier to understand, will be more resilient to change and disruptions, and are much better encapsulated; we can always know what object can operate on a set of data, and in which ways it does this. We centralize the majority of our business logic to these domain objects, and we can entrust them with that because of this encapsulation and overall correctness of behavior.
A rich model, in the context of our code, is expressive. It will use a noun (such as a Book), rather than a semantic abstraction (say BookProcessManagerFactory) and allows us to act on it. Typically this is verb-based — for example book.recommend() to correlate with the actual business terms. As we've seen many times in this book, we want this to explain 1:1 in our common or ubiquitous language what we are doing.
In the below diagram (note that the use case isn't the same as in the last diagram!) you can see how a single Slot Entity (since it's the only one, it gets "promoted" to Aggregate Root; more on this in the next section) is the surface that contains all the data and behavior required to create the slot for a room reservation. It also handles the TimeSlot Value Object as part of the overall Slot. Any changes to the Slot gets pushed as a Domain Event so that we can inform other Aggregates or the rest of the technical landscape of ongoing changes.
Diagram for how user interactions to the Slot ensure the complete transactional boundary for any data it holds.
We expose the operations on the Slot as calls one can make to our API Gateway, firing the relevant Lambda functions that will orchestrate, through use cases, the operations. Thus we can be totally sure that specific operations are only permissible in a deterministic flow, rather than leak it across our complete solution. Any time we are dealing with an Aggregate or Aggregate Root (as we are here, as the Entity is all alone) we publish a Domain Event detailing what happened, such as SlotReserved.

Invariants

Invariants are "consistency rules that must be maintained whenever data changes" (Evans 2004, p. 128). A complete domain model has no holes in it, in other words, there is no possibility for it to be invalid. This is sometimes called the "always-valid" model. I highly recommend reading that link, as we will keep it short here.
To reach an always-valid domain model, what would you need to keep in mind?
  • Your domain model should be valid at all times.
  • For that, do the following:
    • Make the boundary of your domain model explicit.
    • Validate all external requests before they cross that boundary.
    • Don’t use domain classes to carry data that come from the outside world. Use DTOs for that purpose.
    • You cannot strengthen the invariants in your domain model as it would break backward compatibility with the existing data. You need to come up with a transition plan.
See also the following article from Microsoft for more on designing domain-layer validations: https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/domain-model-layer-validations
In plain English, by having moved all the actual domain logic and validations and invariants to the domain layer (where the Entity is the core part), we're already on a good path. We'll see in the code samples some of the basic ways we can handle unique invariants that must be enforced.

Before we move on

Before we go to the code, let's revisit some highlights.
  • Entities are objects that have a unique identity. They are the most closely connected to the domain and its business logic of all DDD concepts.
  • Entities represent our "dumb data" as actual "things" (nouns) and makes it smart by enabling us a programmatic way to interact with the data in a logical manner rather than just supplying getters and setters to a POJO/POCO/JSON object.
  • Entities typically use verbs to express their commands—its public interface.
  • We use the ubiquitous language to name these actions and anything else to do with the Entity.
  • Entities must always be valid. Invariants is the preferred term for our consistency (and validation) rules.

The Slot entity

Be ready for one of our biggest and most important classes, the Slot.
code/Reservation/SlotReservation/src/domain/entities/Slot.ts
1
import { randomUUID } from "crypto";
2
3
import { SlotCreateInput, SlotDTO, Status } from "../../interfaces/Slot";
4
import { TimeSlotDTO } from "../../interfaces/TimeSlot";
5
import { MakeEventInput } from "../../interfaces/Event";
6
7
import { CheckInConditionsNotMetError } from "../../application/errors/CheckInConditionsNotMetError";
8
import { CheckOutConditionsNotMetError } from "../../application/errors/CheckOutConditionsNotMetError";
9
import { CancellationConditionsNotMetError } from "../../application/errors/CancellationConditionsNotMetError";
10
import { ReservationConditionsNotMetError } from "../../application/errors/ReservationConditionsNotMetError";
11
12
/**
13
* @description The `Slot` entity handles the lifecycle
14
* and operations of the (time) slots that users can
15
* reserve.
16
*
17
* @example You can create it at once:
18
* ```
19
* const slot = new Slot({
20
* startTime: "2022-07-29T12:00:00.000Z",
21
* endTime: "2022-07-29T13:00:00.000Z"
22
* });
23
* ```
24
*
25
* You can also reconstitute a Slot from a SlotDTO
26
* loaded from a repository:
27
* ```
28
* const slot = new Slot().fromDto(slotDto);
29
* ```
30
*/
31
export class Slot {
32
private slotId: string;
33
private hostName: string;
34
private timeSlot: TimeSlotDTO;
35
private slotStatus: Status;
36
private createdAt: string;
37
private updatedAt: string;
38
39
constructor(input?: SlotCreateInput) {
40
this.slotId = "";
41
this.hostName = "";
42
this.timeSlot = {
43
startTime: "",
44
endTime: "",
45
};
46
this.slotStatus = "OPEN";
47
this.createdAt = "";
48
this.updatedAt = "";
49
50
if (input) this.make(input);
51
}
52
53
/**
54
* @description Create a valid, starting-state ("open") invariant of the Slot.
55
*/
56
private make(input: SlotCreateInput): SlotDTO {
57
const { startTime, endTime } = input;
58
const currentTime = this.getCurrentTime();
59
60
this.slotId = randomUUID().toString();
61
this.hostName = "";
62
this.timeSlot = {
63
startTime,
64
endTime,
65
};
66
this.slotStatus = "OPEN";
67
this.createdAt = currentTime;
68
this.updatedAt = currentTime;
69
70
return this.toDto();
71
}
72
73
/**
74
* @description Reconstitute a Slot from a Data Transfer Object.
75
*/
76
public fromDto(input: SlotDTO): Slot {
77
this.slotId = input["slotId"];
78
this.hostName = input["hostName"];
79
this.timeSlot = input["timeSlot"];
80
this.slotStatus = input["slotStatus"];
81
this.createdAt = input["createdAt"];
82
this.updatedAt = input["updatedAt"];
83
84
return this;
85
}
86
87
/**
88
* @description Return data as Data Transfer Object.
89
*/
90
public toDto(): SlotDTO {
91
return {
92
slotId: this.slotId,
93
hostName: this.hostName,
94
timeSlot: this.timeSlot,
95
slotStatus: this.slotStatus,
96
createdAt: this.createdAt,
97
updatedAt: this.updatedAt,
98
};
99
}
100
101
/**
102
* @description Remove host name from data.
103
*/
104
public removeHostName(): void {
105
this.hostName = "";
106
}
107
108
/**
109
* @description Update host name to new value.
110
*/
111
public updateHostName(hostName: string): void {
112
this.hostName = hostName;
113
}
114
115
/**
116
* @description Updates the common fields to reflect a new `Status`,
117
* and also updates the `updatedAt` field.
118
*
119
*/
120
public updateStatus(status: Status): void {
121
this.slotStatus = status;
122
this.updatedAt = this.getCurrentTime();
123
}
124
125
/**
126
* @description Returns the start time of the time slot.
127
*/
128
private getStartTime(): string {
129
return this.timeSlot.startTime;
130
}
131
132
/**
133
* @description Has the time slot's end time already passed?
134
*/
135
public isEnded(): boolean {
136
if (this.getCurrentTime() > this.timeSlot.endTime) return true;
137
return false;
138
}
139
140
/**
141
* @description Check if our 10 minute grace period has ended,
142
* in which case we want to open the slot again.
143
*/
144
public isGracePeriodOver(): boolean {
145
if (
146
this.getCurrentTime() >
147
this.getGracePeriodEndTime(this.timeSlot.startTime)
148
)
149
return true;
150
return false;
151
}
152
153
/**
154
* @description Returns the end of the grace period until a reserved
155
* slot is deemed unattended and returns to open state.
156
*/
157
private getGracePeriodEndTime(startTime: string): string {
158
const minutes = 10;
159
const msPerMinute = 60 * 1000;
160
161
return new Date(
162
new Date(startTime).getTime() + minutes * msPerMinute
163
).toISOString();
164
}
165
166
/**
167
* @description Returns the current time as an ISO string.
168
*/
169
private getCurrentTime(): string {
170
return new Date().toISOString();
171
}
172
173
/**
174
* @description Can this `Slot` be cancelled?
175
*/
176
private canBeCancelled(): boolean {
177
if (this.slotStatus !== "RESERVED") return false;
178
return true;
179
}
180
181
/**
182
* @description Can this `Slot` be reserved?
183
*/
184
private canBeReserved(): boolean {
185
if (this.slotStatus !== "OPEN") return false;
186
return true;
187
}
188
189
/**
190
* @description Can this `Slot` be checked in to?
191
*/
192
private canBeCheckedInTo(): boolean {
193
if (this.slotStatus !== "RESERVED") return false;
194
return true;
195
}
196
197
/**
198
* @description Can this `Slot` be checked out of?
199
*/
200
private canBeCheckedOutOf(): boolean {
201
if (this.slotStatus !== "CHECKED_IN") return false;
202
return true;
203
}
204
205
/**
206
* @description Can this `Slot` be unattended?
207
*/
208
private canBeUnattended(): boolean {
209
if (this.slotStatus === "RESERVED") return true;
210
return false;
211
}
212
213
/**
214
* @description Updates a Slot to be in `OPEN` invariant state by cancelling the current state.
215
*
216
* Can only be performed in `RESERVED` state.
217
*
218
* @emits `CANCELLED`
219
*/
220
public cancel(): SlotCommand {
221
if (!this.canBeCancelled())
222
throw new CancellationConditionsNotMetError(this.slotStatus);
223
224
const newStatus = "OPEN";
225
226
this.removeHostName();
227
this.updateStatus(newStatus);
228
229
return {
230
event: {
231
eventName: "CANCELLED", // Transient state
232
slotId: this.slotId,
233
slotStatus: this.slotStatus,
234
hostName: this.hostName,
235
startTime: this.getStartTime(),
236
},
237
newStatus,
238
};
239
}
240
241
/**
242
* @description Updates a Slot to be in `RESERVED` invariant state.
243
*
244
* Can only be performed in `OPEN` state.
245
*
246
* @emits `RESERVED`
247
*/
248
public reserve(hostName: string): SlotCommand {
249
if (!this.canBeReserved())
250
throw new ReservationConditionsNotMetError(this.slotStatus);
251
252
const newStatus = "RESERVED";
253
254
this.updateHostName(hostName || "");
255
this.updateStatus(newStatus);
256
257
return {
258
event: {
259
eventName: newStatus,
260
slotId: this.slotId,
261
slotStatus: newStatus,
262
hostName: this.hostName,
263
startTime: this.getStartTime(),
264
},
265
newStatus,
266
};
267
}
268
269
/**
270
* @description Updates a Slot to be in `CHECKED_IN` invariant state.
271
*
272
* Can only be performed in `RESERVED` state.
273
*
274
* @emits `CHECKED_IN`
275
*/
276
public checkIn(): SlotCommand {
277
if (!this.canBeCheckedInTo())
278
throw new CheckInConditionsNotMetError(this.slotStatus);
279
280
const newStatus = "CHECKED_IN";
281
this.updateStatus(newStatus);
282
283
return {
284
event: {
285
eventName: newStatus,
286
slotId: this.slotId,
287
slotStatus: newStatus,
288
hostName: this.hostName,
289
startTime: this.getStartTime(),
290
},
291
newStatus,
292
};
293
}
294
295
/**
296
* @description Updates a Slot to be in `OPEN` invariant state by checking out from the current state.
297
*
298
* Can only be performed in `CHECKED_IN` state.
299
*
300
* @emits `CHECKED_OUT`
301
*/
302
public checkOut(): SlotCommand {
303
if (!this.canBeCheckedOutOf())
304
throw new CheckOutConditionsNotMetError(this.slotStatus);
305
306
const newStatus = "OPEN";
307
this.updateStatus(newStatus);
308
this.removeHostName();
309
310
return {
311
event: {
312
eventName: "CHECKED_OUT", // Transient state
313
slotId: this.slotId,
314
slotStatus: newStatus,
315
hostName: this.hostName,
316
startTime: this.getStartTime(),
317
},
318
newStatus,
319
};
320
}
321
322
/**
323
* @description Updates a Slot to be in "open" invariant state.
324
*
325
* @emits `OPENED`
326
*/
327
public open(): SlotCommand {
328
const newStatus = "OPEN";
329
this.updateStatus(newStatus);
330
331
return {
332
event: {
333
eventName: "OPENED",
334
slotId: this.slotId,
335
slotStatus: newStatus,
336
hostName: "",
337
startTime: this.getStartTime(),
338
},
339
newStatus,
340
};
341
}
342
343
/**
344
* @description Updates a Slot to be in "closed" invariant state.
345
*
346
* @emits `CLOSED`
347
*/
348
public close(): SlotCommand {
349
const newStatus = "CLOSED";
350
this.updateStatus(newStatus);
351
352
return {
353
event: {
354
eventName: newStatus,
355
slotId: this.slotId,
356
slotStatus: newStatus,
357
hostName: this.hostName,
358
startTime: this.getStartTime(),
359
},
360
newStatus,
361
};
362
}
363
364
/**
365
* @description Set a slot as being in `OPEN` invariant state if it is unattended.
366
*
367
* State change can only be performed in `RESERVED` state.
368
*
369
* This is only triggered by scheduled events.
370
*
371
* @emits `UNATTENDED`
372
*/
373
public unattend(): SlotCommand | void {
374
if (!this.canBeUnattended()) return;
375
376
const newStatus = "OPEN";
377
this.updateStatus(newStatus);
378
this.removeHostName();
379
380
return {
381
event: {
382
eventName: "UNATTENDED", // Transient state
383
slotId: this.slotId,
384
slotStatus: newStatus,
385
hostName: this.hostName,
386
startTime: this.getStartTime(),
387
},
388
newStatus,
389
};
390
}
391
}
392
393
/**
394
* @description The finishing command that the `Slot` sends back when done.
395
*/
396
export interface SlotCommand {
397
event: MakeEventInput;
398
newStatus: Status;
399
}
There's a bunch of private and public methods here, with a slightly higher public method count than on the private side. You'll notice that there are a couple of patterns that keep repeating like those that return SlotCommand and those that check rules.
It might have been more "effective" in a strict, technocratic sense to leave removeHostName(), updateStatus() and getCurrentTime() out as functions and just directly manipulate the values. I am sure you know I will complain about how that breaks our possibility to encapsulate and truly trust our provided mechanisms if we gave even an inch away on this matter.

The constructor

Let's see:
private slotId: string;
private hostName: string;
private timeSlot: TimeSlotDTO;
private slotStatus: Status;
private createdAt: string;
private updatedAt: string;
constructor(input?: SlotCreateInput) {
this.slotId = '';
this.hostName = '';
this.timeSlot = {
startTime: '',
endTime: ''
};
this.slotStatus = 'OPEN';
this.createdAt = '';
this.updatedAt = '';
if (input) this.make(input);
}
Our internal private fields represent the data we store. They can't be retrieved from outside the class which is perfect—this is one of the easiest but most important wins when using DDD or good OOP for that matter. Now, users will have to use our exposed public methods to actually mutate our data.
When constructed, if we lack input, we will assume an almost barren state. We've also set up a basic private make() method that will return back the starting-state invariant which we call "open" if slot creation input is passed in.
/**
* @description Create a valid, starting-state ("open") invariant of the Slot.
*/
private make(input: SlotCreateInput): SlotDTO {
const { startTime, endTime } = input;
const currentTime = this.getCurrentTime();
this.slotId = randomUUID().toString();
this.hostName = '';
this.timeSlot = {
startTime,
endTime
};
this.slotStatus = 'OPEN';
this.createdAt = currentTime;
this.updatedAt = currentTime;
return this.toDto();
}

Reconstitute from a DTO

Now for one of the most important private methods: fromDto(). This will enable us to create a class representation (Slot Entity) from a Data Transfer Object. It's nothing hard nor magical, just:
/**
* @description Reconstitute a Slot from a Data Transfer Object.
*/
public fromDto(input: SlotDTO): Slot {
this.slotId = input['slotId'];
this.hostName = input['hostName'];
this.timeSlot = input['timeSlot'];
this.slotStatus = input['slotStatus'];
this.createdAt = input['createdAt'];
this.updatedAt = input['updatedAt'];
return this;
}
This acts as our public setter method. In this case we can practically always trust the input but an improvement would be to add validation logic at this point.
By returning a reference to the instance we can allow chaining of commands making the programmatic use a little easier.

Make into a DTO

There is no way for us to transport a class across systems, so we will have to represent the key data in some way. Luckily this is easy.
/**
* @description Return data as Data Transfer Object.
*/
public toDto(): SlotDTO {
return {
slotId: this.slotId,
hostName: this.hostName,
timeSlot: this.timeSlot,
slotStatus: this.slotStatus,
createdAt: this.createdAt,
updatedAt: this.updatedAt
};
}
The fields act as a well-known interface/type (SlotDTO) and we can now trivially pass this to our persistence mechanism or elsewhere where we don't, or can't, use the actual Slot Entity class.

Use case #1: Domain logic for checking if we can reserve and cancel

Business logic. Domain logic. Both sound big. Dangerous. In our case it's literally a check on the expected, valid slotStatus.
1
/**
2
* @description Can this `Slot` be reserved?
3
*/
4
private canBeReserved(): boolean {
5
if (this.slotStatus !== 'OPEN') return false;
6
return true;
7
}
Now that's some nice, basic logic right there! No need for enums or anything, we just need to check for an open status.
/**
* @description Can this `Slot` be cancelled?
*/
private canBeCancelled(): boolean {
if (this.slotStatus !== 'RESERVED') return false;
return true;
}
The same goes for the cancellation check, we need to know if we are reserved or not. Both, as seen, return boolean results which makes it a simple-to-understand and expressive check.
Nothing is blocking you to conduct much deeper checking, though that seems overboard in our example code.

Use case #2: Is the grace period over?

Our Domain Service, ReservationService, calls each Slot's isGracePeriodOver() method when checking if we have any reservations that have expired their 10-minute grace period.
1
/**
2
* @description Check if our 10 minute grace period has ended,
3
* in which case we want to open the slot again.
4
*/
5
public isGracePeriodOver(): boolean {
6
if (this.getCurrentTime() > this.getGracePeriodEndTime(this.timeSlot.startTime)) return true;
7
return false;
8
}
9
10
/**
11
* @description Returns the end of the grace period until a reserved
12
* slot is deemed unattended and returns to open state.
13
*/
14
private getGracePeriodEndTime(startTime: string): string {
15
const minutes = 10;
16
const msPerMinute = 60 * 1000;
17
18
return new Date(new Date(startTime).getTime() + minutes * msPerMinute).toISOString();
19
}
The internal logic is here a tiny bit more elaborate than the super-simple ones from the last example. All the logic around this is neatly stored within the Entity and we are left with a clean, nice public interface to get our answer.

Use case #3: Reserving a slot

Here's now an example of the actual reservation logic.
1
/**
2
* @description Updates a Slot to be in `RESERVED` invariant state.
3
*
4
* Can only be performed in `OPEN` state.
5
*
6
* @emits `RESERVED`
7
*/
8
public reserve(hostName: string): SlotCommand {
9
if (!this.canBeReserved()) throw new ReservationConditionsNotMetError(this.slotStatus);
10
11
const newStatus = 'RESERVED';
12
13
this.updateHostName(hostName || '');
14
this.updateStatus(newStatus);
15
16
return {
17
event: {
18
eventName: newStatus,
19
slotId: this.slotId,
20
slotStatus: newStatus,
21
hostName: this.hostName,
22
startTime: this.getStartTime()
23
},
24
newStatus
25
};
26
}
It will throw an error if it cannot be reserved, which is cruder than how we could do it. Nevertheless, this seems like a reasonable version 1 of our solution. Next, we will set a new status, update the host name and status internally, and then return a SlotCommand which is a type of object that we can create an actual Domain Event from later. Note how, at this point, we have not persisted anything, just made sure that it's all valid, our object is in a regulated and valid state, and that we feed back the basis of our upcoming event for our integration purposes.
Last modified 6mo ago