In the conversation with Jana and at the event-storming wall you ran into words that mean two things. So far you have resolved them in the glossary by giving each thing a word of its own. That doesn't always work, and it doesn't always have to. When the consulting side talks about “the contact” of a quote, it means something different from what Mia means in the contact list, and both are right. DDD's answer is called bounded context: a boundary inside which a word has exactly one meaning. In this lesson you cut the Kladde along such boundaries.
One word, two models
Take the contact. In the contact list, a contact is a person with their current data: name, email address, organization, tags, maybe a birthday. If she moves or changes her email address, Mia changes the contact, and that is right.
In the quote, “the contact” is the contact person the quote was addressed to. For a sent quote, that is a fact of the past: the quote went to this address, with this salutation, on this day. If the address later changes in the contact list, the sent quote must not change along with it. Otherwise the Kladde shows a quote that was never sent that way.
A single model for both can get only one of them right. Two models get both right:
// two-models.cs
// One word, two models: "contact" in the contacts and as the contact person in the quote.
// Start: dotnet run two-models.cs
using Kladde.Contacts;
using Kladde.Quotes;
var contacts = new ContactList();
var dana = contacts.Add("Dana Kranich", "kranich@pond-elementary.example", "Pond Elementary School");
dana.Tag("Principal");
dana.Tag("Tablets");
// At the draft, the quote takes over what it needs from the contact person: a snapshot.
var quote = new Quote("A-17", new ContactPerson(dana.Id, dana.Name, dana.Email, "Principal"));
Show(contacts, quote);
// Ms. Kranich gets a new email address. The contacts change, the sent quote does not.
dana.ChangeEmail("d.kranich@school-district.example");
Console.WriteLine("After the new email address:");
Show(contacts, quote);
static void Show(ContactList contacts, Quote quote)
{
foreach (var contact in contacts.All)
{
Console.WriteLine($" Contacts: {contact.Name} <{contact.Email}>, {contact.Organization}, Tags: {string.Join(", ", contact.Tags)}");
}
Console.WriteLine($" Quotes: {quote.Number} to {quote.Recipient.Name} ({quote.Recipient.Role}) <{quote.Recipient.Email}>");
}
namespace Kladde.Contacts
{
public readonly record struct ContactId(int Value);
// A contact in the contacts: the person with their current data. It changes when the person changes.
public sealed class Contact(ContactId id, string name, string email, string organization)
{
private readonly List<string> tags = [];
public ContactId Id { get; } = id;
public string Name { get; private set; } = name;
public string Email { get; private set; } = email;
public string Organization { get; private set; } = organization;
public IReadOnlyList<string> Tags => tags;
public void ChangeEmail(string email) => Email = email;
public void Tag(string tag) => tags.Add(tag);
}
public sealed class ContactList
{
private readonly List<Contact> contacts = [];
public IReadOnlyList<Contact> All => contacts;
public Contact Add(string name, string email, string organization)
{
var contact = new Contact(new ContactId(contacts.Count + 1), name, email, organization);
contacts.Add(contact);
return contact;
}
}
}
namespace Kladde.Quotes
{
using Kladde.Contacts;
// The contact person in the quote: whom the quote went to, as it was at the draft. It never changes.
public sealed record ContactPerson(ContactId ContactId, string Name, string Email, string Role);
public sealed record Quote(string Number, ContactPerson Recipient);
}
dotnet run two-models.cs
Contacts: Dana Kranich <kranich@pond-elementary.example>, Pond Elementary School, Tags: Principal, Tablets
Quotes: A-17 to Dana Kranich (Principal) <kranich@pond-elementary.example>
After the new email address:
Contacts: Dana Kranich <d.kranich@school-district.example>, Pond Elementary School, Tags: Principal, Tablets
Quotes: A-17 to Dana Kranich (Principal) <kranich@pond-elementary.example>
What the example shows:
- The same person, two models.
Contactis mutable and has tags;ContactPersonis an immutable record with a role in the quote. Neither knows anything about the other's fields that it doesn't need. - The two are permanently connected only through the ID.
ContactPersonremembers theContactId, so you can jump from the quote to the current contact. The quote takes over the rest once, at the draft, as a snapshot. - The snapshot is a business decision, not a technical one. Asked whether a sent quote should show the old or the new address, every expert answers: the one it went to.
- It has a consequence for data protection. Name and email address now stand in two contexts. That is fine, but it has to be known: in lesson 4.2.6 you mark which context holds personal data, and in module 4.8 you learn how to delete it anyway.
The example has both namespaces in one file so it runs as a file-based app. In the Kladde they become two projects from module 4.3 on.