Domain-Driven Design wordt vaak geassocieerd met strategische patronen - bounded contexts, ubiquitous language, context mapping. Maar de tactische patronen zijn minstens zo belangrijk. En het patroon dat we het vaakst verkeerd toegepast zien, is het aggregate. Laten we dat rechtzetten.

Wat is een aggregate?

Een aggregate is een cluster van domeinobjecten die samen worden behandeld als één eenheid voor datawijzigingen. Elk aggregate heeft een root entity - het enige object waar externe code direct naar mag verwijzen. Alle wijzigingen aan het aggregate verlopen via de root.

Klinkt abstract? Een concreet voorbeeld: een Bestelling (aggregate root) bevat Orderregels (child entities). Je voegt geen orderregel toe door direct de lijst te manipuleren. Je roept bestelling.VoegRegelToe(product, aantal) aan. De bestelling bewaakt de regels.

Invarianten: de regels die altijd gelden

De belangrijkste reden voor aggregates is het beschermen van invarianten - bedrijfsregels die op elk moment waar moeten zijn. Voorbeelden:

  • Een bestelling mag niet meer dan 50 regels bevatten
  • Het totaalbedrag van een bestelling mag nooit negatief zijn
  • Een reservering mag niet overlappen met een bestaande reservering voor dezelfde resource

Zonder aggregates verspreid je deze validatielogica over services, controllers en repositories. Het resultaat: inconsistente data en bugs die alleen in productie opduiken.

Met aggregates centraliseer je de invarianten. De aggregate root is de poortwachter. Elke toestandswijziging passeert de root, en de root garandeert dat alle invarianten intact blijven.

De valkuil van te grote aggregates

De meest voorkomende fout: aggregates die te groot zijn. We hebben systemen gezien waar een "Klant"-aggregate de klantgegevens, alle bestellingen, alle facturen en het volledige communicatielogboek omvat. Dat leidt tot:

  • Performance-problemen - het laden van een aggregate vereist het ophalen van enorme objectgrafen
  • Concurrency-conflicten - twee gebruikers die onafhankelijke wijzigingen doen, blokkeren elkaar
  • Tight coupling - wijzigingen aan facturatielogica raken het klant-aggregate

Hoe bepaal je de juiste grens?

De vuistregel van Vaughn Vernon is helder: maak aggregates zo klein mogelijk. Bescherm alleen de invarianten die echt transactioneel consistent moeten zijn. Alles wat eventually consistent mag zijn, hoort in een apart aggregate.

Stel jezelf deze vragen:

  1. Welke gegevens moeten in dezelfde transactie gewijzigd worden? Dat is je aggregate.
  2. Kan een businessregel gevalideerd worden met gegevens uit een enkel aggregate? Zo niet, heroverweeg je grenzen.
  3. Leidt je aggregate tot concurrency-conflicten? Dat is een signaal dat het te groot is.

In de praktijk betekent dit vaak dat een Bestelling en een Klant aparte aggregates zijn. De bestelling verwijst naar de klant via een ID, niet via een directe objectreferentie.

Aggregates en persistence

Een aggregate wordt altijd in zijn geheel opgeslagen en geladen. Dit is de reden dat de grootte ertoe doet. Met Entity Framework betekent dit vaak dat je een Include-keten hebt die exact de grens van je aggregate volgt. Met event sourcing betekent het dat je de events van één aggregate-instantie replayed om de huidige staat te reconstrueren.

Belangrijk: repositories werken op aggregate-niveau. Je hebt een IBestellingRepository, niet een IOrderregelRepository. De aggregate root is de eenheid van persistence.

Praktisch aan de slag

Begin met het identificeren van je invarianten. Niet de technische constraints (dat doet je database), maar de business invarianten - de regels die je domeinexperts je vertellen. Groepeer de gegevens die samen die invarianten beschermen. Dat is je aggregate.

Houd ze klein, houd ze gefocust, en laat ze communiceren via domein-events. Zo bouw je software die robuust is en meegroeit met de complexiteit van je domein.

Bij NForza passen we DDD toe in de praktijk - niet als academische oefening, maar als gereedschap voor software die jarenlang meegaat.