Det taktiska och strategiska DDD som krävs för att förstå aggregat. Ubiquitous Language, Bounded Context, Entity, Value Object och Domain Event — så att vi i nästa lektion kan koda en riktig AggregateRoot.
DDD är en uppsättning strategiska verktyg för att dela upp ett stort system efter affärsdomäner, och taktiska verktyg för att modellera koden inuti varje domän så att den speglar verksamhetens språk. Boken som myntade begreppen är Eric Evans Domain-Driven Design från 2003 — fortfarande den bästa källan.
Ett gemensamt språk för utvecklare, domänexperter och dokumentation. Om verksamheten säger "policy" så heter klassen Policy — inte InsuranceContract. Översättningsfriktion är en ständig källa till buggar.
Ett bounded context är en gräns där en modell (och ett språk) gäller. Ordet "Customer" betyder olika saker i Försäljning, Fakturering och Support — varje kontext har sin egen Customer-klass. Att försöka tvinga in en gemensam modell är klassisk källa till stora, oförståeliga system.
// Sales-kontexten
namespace TicketHub.Sales;
public class Customer
{
public CustomerId Id { get; }
public string Name { get; }
public LoyaltyTier Tier { get; } // bara intressant vid försäljning
}
// Billing-kontexten
namespace TicketHub.Billing;
public class Customer
{
public CustomerId Id { get; }
public Address BillingAddress { get; }
public VatNumber? Vat { get; } // bara intressant vid fakturering
}
De två klasserna delar bara ett id. Översättning mellan kontexter sker via context maps (Shared Kernel, Customer/Supplier, Anti-Corruption Layer m.fl.) — i kursen håller vi oss till en kontext per gång.
| Byggsten | Identitet | Föränderlig? | Exempel |
|---|---|---|---|
| Value Object | Nej — likhet via värde | Nej (immutable) | Money, Address, DateRange |
| Entity | Ja — har ID | Ja (genom tid) | Customer, Booking, Account |
| Aggregate Root | Ja — entitet som är "ingång" | Ja | Order (med OrderLine-children) |
| Domain Event | Ja — har eventId | Nej (har redan hänt) | MoneyDeposited, SeatReserved |
| Domain Service | Nej | Statslös | ExchangeRateService |
| Repository | — | — | IAccountRepository (load/save aggregat) |
Sedan C# 9 är record-typer med strukturell likhet det naturliga valet:
public sealed record Money(decimal Amount, string Currency)
{
public static Money Zero(string currency) => new(0m, currency);
public Money Add(Money other)
{
if (other.Currency != Currency)
throw new InvalidOperationException(
$"Cannot add {other.Currency} to {Currency}");
return this with { Amount = Amount + other.Amount };
}
public Money Subtract(Money other) => Add(other with { Amount = -other.Amount });
public override string ToString() => $"{Amount:0.00} {Currency}";
}
with). Inga ID-fält. Likhet via värde — får i C# automatiskt med record.
Skicka inte runt Guid:er — slå in dem i typer som inte går att blanda ihop:
public readonly record struct AccountId(Guid Value)
{
public static AccountId New() => new(Guid.NewGuid());
public override string ToString() => Value.ToString("N");
}
public readonly record struct CustomerId(Guid Value);
// Kompilatorn hjälper dig:
void Transfer(AccountId from, AccountId to, Money amount);
// Transfer(customerId, accountId, ...) går inte att skriva av misstag.
Entiteter har identitet som överlever förändring. Två insättningar på 500 kr är inte samma sak — varje är en egen entitet med eget id, även om de har samma belopp.
public sealed class Booking
{
public BookingId Id { get; }
public SeatId Seat { get; private set; }
public BookingStatus Status { get; private set; }
// ... beteende, inte bara properties
}
Ett domain event är en kort, oföränderlig beskrivning av något som har hänt. Namnges i dåtid. Innehåller all data som behövs för att läsare ska kunna agera — utan att fråga tillbaka.
public interface IDomainEvent
{
Guid EventId { get; }
DateTimeOffset OccurredAt { get; }
}
public sealed record MoneyDeposited(
Guid EventId,
DateTimeOffset OccurredAt,
AccountId Account,
Money Amount,
string Reference) : IDomainEvent;
public sealed record SeatReserved(
Guid EventId,
DateTimeOffset OccurredAt,
ConcertId Concert,
SeatId Seat,
CustomerId By) : IDomainEvent;
DepositMoney (command, imperativ) → MoneyDeposited (event, dåtid).
Ett aggregat är en grupp av entiteter och värdeobjekt som måste vara konsistenta tillsammans. Aggregate root är den enda som omvärlden får referera till — all åtkomst till barn-entiteterna sker genom roten.
Regler (som vi fördjupar i nästa lektion):
Ett repository ser ut som en samling av aggregat i minnet. Implementeringen kan vara EF Core, Dapper, in-memory eller — som i denna kurs — SqlStreamStore.
public interface IAccountRepository
{
Task<Account?> LoadAsync(AccountId id, CancellationToken ct = default);
Task SaveAsync(Account aggregate, CancellationToken ct = default);
}
Notera frånvaron av Update, Find, Query. Repositoriet hanterar bara hela aggregat — frågor ställs mot read models (CQRS, lektion 4).
Event Storming (Alberto Brandolini) är en effektiv teknik: samla domänexperter och utvecklare framför en vägg med post-it-lappar. Orange = events (dåtid), blå = commands, gula = aggregat. Mönstren framträder snabbt och språket blir verkligen ubiquitous.
Lös övningarna självständigt. Det finns inget facit — lärandet sker i processen.
EmailAddress som value object i C# med record. Validera formatet i konstruktorn. Lägg till en Domain-property som returnerar delen efter @. Testa likhet mellan två instanser.Guid för flera olika entiteter till strongly typed IDs. Visa minst ett fall där kompilatorn nu hindrar en bugg som tidigare hade gått igenom.Domain-projektet. Skriv 10+ enhetstester som beskriver beteendet.
Order) ser olika ut i de två. Rita en kontextkarta som visar relationen (Shared Kernel? Customer/Supplier? Anti-Corruption Layer?). Motivera valet.