CQRS implementeren in Go: een praktische gids voor schaalbare architectuur

Bouw CQRS in Go zonder overbodige complexiteit

Inhoud

CQRS is een van die patronen die te vaak worden overschat, overcompliceerd en af en toe ten onrechte worden voorgeschoten als geneesmiddel voor de saaiheid van standaard CRUD-operaties.

De nuttige versie is veel simpeler: scheid de code die de status verandert van de code die de status leest, en laat elke kant zich onafhankelijk ontwikkelen voor zijn eigen taak. Martin Fowler beschrijft CQRS als het gebruik van een ander model om informatie te updaten dan het model dat wordt gebruikt om deze te lezen, terwijl hij ook waarschuwt dat dit voor de meeste systemen risicovol complexiteit toevoegt. Microsoft maakt dezelfde kernpleit in meer operationele termen: scheid lees- en schrijfm Modellen zodat elk onafhankelijk kan worden geoptimaliseerd.

CQRS in Go — commando’s en queries als aparte paden door een Go-gopher hub

Als je in Go werkt, komt dit idee ongebruikelijk goed overeen met de taal. Go is goed in expliciete grenzen, kleine interfaces, saaie gegevenstypen en pakketten die gericht zijn op use-cases. Dit maakt basis-CQRS in Go veel minder theatralisch dan het vaak lijkt op conferentieslide-decks. Je hebt geen event sourcing, Kafka of drie databases nodig om te beginnen. In feite tonen zowel de CQRS-richtlijnen van Microsoft als de Go-voorbeelden van Three Dots Labs aan dat een eenvoudige implementatie dezelfde onderliggende opslag kan delen, met aparte commando- en query-handlers die eerst worden toegevoegd en fancy infrastructuur die pas wordt geïntroduceerd wanneer het probleem dit daadwerkelijk vereist.

Wat CQRS eigenlijk betekent

In de kern trekt CQRS een harde lijn tussen commando’s en queries. Een query leest gegevens en mag de status van het systeem niet wijzigen. Een commando verandert de status en mag geen domeingegevens als hoofdresultaat teruggeven. Three Dots Labs formuleren dit in praktische Go-termen: queries retourneren gegevens en commando’s maken wijzigingen, waarbij fouten een normaal commandoresultaat zijn. Dat is de basisbeweging. Alles anders is optioneel.

Een veelvoorkomend misverstand is dat CQRS automatisch betekent dat er aparte databases, asynchrone projecties of event sourcing nodig zijn. Dat is niet waar. De patronengids van Microsoft behandelt aparte gegevensopslag expliciet als de meer geavanceerde vorm, niet als de standaard, en Three Dots Labs tonen een Go-implementatie waarin queries lezen uit dezelfde database als schrijft, omdat dat voldoende is voor het betreffende systeem. Als je artikel maar één ding duidelijk leert, laat het dit dan zijn: CQRS is primair een keuze voor modellering en applicatiestructuur, geen verplichte deal voor gedistribueerde systemen.

Het andere belangrijke detail is de naamgeving. Commando’s moeten de bedrijfsintentie modelleren, niet opslagmutaties. Het voorbeeld van Microsoft contrasteert “Hotelkamer boeken” met “Reserveringsstatus instellen op Gereserveerd”, en Three Dots Labs adviseren namen die dicht bij de manier staan waarop domeinexperts spreken, zoals “TrainingPlannen” of “TrainingAnnuleren” in plaats van generieke werkwoorden als “Aanmaken” en “Verwijderen”. In Go betaalt deze naamdicipline zich omdat commandonamen vaak typenamen, handlernamen en pakketgrenzen worden.

Waarom teams er naar grijpen

CQRS wordt aantrekkelijk wanneer een enkel CRUD-model te veel taken slecht begint uit te voeren. De richtlijnen van Microsoft noemen de gebruikelijke drukpunten: de lees- en schrijfrepresentaties van dezelfde gegevens wijken van elkaar af, gelijktijdige updates veroorzaken lock-contentie, de leesprestaties lijden onder querycomplexiteit en gedeelde entiteiten maken van beveiligingsregels een wirwar. Met andere woorden, het probleem is niet dat CRUD moreel verkeerd is. Het probleem is dat één model wordt gedwongen om tegelijkertijd incompatibele eisen te vervullen.

Dat komt vooral vaak voor in technische producten. Schrijven heeft de neiging zich te richten op validatie, invarianten, transacties en bedrijfsregels. Lezen heeft de neiging zich te richten op filters, joins, aggregatie, caching, sortering en het leveren van precies de vorm die een pagina of API nodig heeft. CQRS laat de schrijfzijde streng en domeingeoriënteerd blijven, terwijl de leeszijde pragmatisch en DTO-georiënteerd blijft. Microsoft adviseert expliciet een schrijfmodel gericht op validatie en consistentie, en een leesmodel gericht op DTO’s of projecties die zijn geoptimaliseerd voor presentatie en responsiviteit.

Er is ook een teamniveau-voordeel. Three Dots Labs stellen dat het splitsen van commando’s en queries de ontkoppeling verbetert, de uitvoeringsstroom duidelijker maakt en het onboarding versnelt, omdat ontwikkelaars een kleine lijst van beschikbare commando’s en queries kunnen inspecteren in plaats van logica door willekeurige servicelagen te achtervolgen. Microsoft merkt op dat CQRS vooral nuttig is in samenwerkingsomgevingen waarbij meerdere gebruikers dezelfde gegevens updaten en commando’s genoeg granulariteit moeten hebben om conflicten te voorkomen of op te lossen.

Mijn iets eigenzinnige visie is dit: de meeste teams adopteren CQRS te laat, nadat één “service” al is omgevormd tot een zacht-monolith. Maar veel teams adopteren het ook te vroeg, voornamelijk omdat het architectuurschema duur en daarom serieus leek. Het juiste moment is wanneer lees- en schrijfoperaties duidelijk uit elkaar beginnen te drijven in vorm, snelheid of regels, niet wanneer je todo-app ambities heeft.

De voordelen en de rekening

Basis-CQRS heeft echte voordelen, zelfs voordat je messaging of aparte opslag toevoegt. Het geeft je kleinere commandomodellen, kleinere querymodellen, duidelijkere use-cases en meer voor de hand liggende plekken om cross-cutting concerns zoals logging en instrumentation toe te passen. Three Dots Labs noemen expliciet betere codeorganisatie, ontkoppeling en eenvoudigere modellen als directe winsten, terwijl Microservices.io eenvoudigere commando- en querymodellen en ondersteuning voor gedenoormaliseerde, schaalbare leesweergaven benadrukt.

Zodra het probleem dit rechtvaardigt, opent CQRS ook de deur naar sterkere optimalisatie aan de leeszijde. De richtlijnen van Microsoft noteren dat aparte leesmodellen DTO’s, projecties, read-only-replicas of zelfs een volledig andere opslagtechnologie kunnen gebruiken. Het wijst ook op materialised views als een manier om zware joins en ORM-zware querypaden te vermijden. Als je evalueert welke data-toegangslaag je aan de schrijfzijde wilt gebruiken, behandelt Vergelijking van Go ORMs voor PostgreSQL de afwegingen tussen GORM, Ent, Bun en sqlc in praktische termen. Dat is waar CQRS operationeel begint af te betalen, niet alleen structureel.

De kosten zijn eveneens echt. De waarschuwing van Fowler is nog steeds het juiste startpunt: voor de meeste systemen voegt CQRS risicovol complexiteit toe. Microsoft noemt verhoogde complexiteit en eventuele consistentie als kernoverwegingen, terwijl Microservices.io potentiële codeduplicatie en replicatieterugstand in leesweergaven toevoegt. Als je opslag splitst, erf je ook de taak om ze op elkaar af te stemmen, meestal via events, zonder te vertrouwen op een nette gedistribueerde transactie tussen je database en broker.

Event sourcing vermindert die rekening niet; het verandert de vorm ervan. De CQRS-richtlijnen van Microsoft stellen dat event sourcing de event store de enige bron van waarheid kan maken en je materialised views kunt herbouwen door de geschiedenis te spelen, terwijl Event Horizon wijst op traceerbaarheid en audit logging als grote voordelen. Maar Microsoft waarschuwt ook dat viewgeneratie, playback en eventverwerking meer ontwerpcomplexiteit toevoegen, en suggereert snapshots om playback-kosten te verminderen. Daarom verkies ik event sourcing uit te leggen als “CQRS plus een tweede moeilijke beslissing”, niet als het instapticket.

Een nuttige vuistregel om in gedachten te houden is dat basis-CQRS goedkoop is terwijl gedistribueerd CQRS duur is, en het vermengen van deze twee gesprekken is een van de meest voorkomende manieren waarop teams eindigen met veel meer complexiteit dan het probleem ooit vereiste.

Een eenvoudige CQRS-implementatie in Go

Een verstandige eerste stap in Go is om één database te behouden en alleen de applicatielaag te splitsen. Commando’s beheersen bedrijfsregels en persistentie. Queries retourneren leesmodellen die zijn gevormd voor aanroepers. Dit is precies het soort basis-CQRS dat Three Dots Labs aanbevelen voordat je grijpt naar asynchrone bussen of aparte leesopslag.

Begin met commando’s

package blog

import (
	"context"
	"errors"
	"time"
)

type PublishPostCommand struct {
	Title   string
	Slug    string
	BodyMD  string
	Author  string
}

type PostRepository interface {
	NextID(ctx context.Context) (string, error)
	Save(ctx context.Context, post Post) error
}

type Post struct {
	ID          string
	Title       string
	Slug        string
	BodyMD      string
	Author      string
	PublishedAt time.Time
}

type PublishPostHandler struct {
	Repo  PostRepository
	Now   func() time.Time
}

func (h PublishPostHandler) Handle(ctx context.Context, cmd PublishPostCommand) error {
	if cmd.Title == "" || cmd.Slug == "" || cmd.BodyMD == "" {
		return errors.New("titel, slug en body zijn verplicht")
	}

	id, err := h.Repo.NextID(ctx)
	if err != nil {
		return err
	}

	post := Post{
		ID:          id,
		Title:       cmd.Title,
		Slug:        cmd.Slug,
		BodyMD:      cmd.BodyMD,
		Author:      cmd.Author,
		PublishedAt: h.Now(),
	}

	return h.Repo.Save(ctx, post)
}

Deze handler probeert geen pagina te dienen, een lijstresponse te vormen of SQL te optimaliseren voor een card-grid. Het afdwingen van intentie en het persisten van een geldige aggregate is de enige taak. Dat is de commandozijde die één taak goed doet.

Voeg queries toe

package blog

import "context"

type PostView struct {
	ID          string
	Title       string
	Slug        string
	Author      string
	PublishedAt string
	Excerpt     string
}

type LatestPostsQuery struct {
	Limit int
}

type PostReadModel interface {
	Latest(ctx context.Context, limit int) ([]PostView, error)
	BySlug(ctx context.Context, slug string) (PostView, error)
}

type LatestPostsHandler struct {
	ReadModel PostReadModel
}

func (h LatestPostsHandler) Handle(ctx context.Context, q LatestPostsQuery) ([]PostView, error) {
	limit := q.Limit
	if limit <= 0 {
		limit = 10
	}
	return h.ReadModel.Latest(ctx, limit)
}

type GetPostBySlugQuery struct {
	Slug string
}

type GetPostBySlugHandler struct {
	ReadModel PostReadModel
}

func (h GetPostBySlugHandler) Handle(ctx context.Context, q GetPostBySlugQuery) (PostView, error) {
	return h.ReadModel.BySlug(ctx, q.Slug)
}

Merk op dat de leeszijde een PostView retourneert, niet het schrijfmodel. Dit weerspiegelt de aanbeveling van Microsoft dat het leesmodel moet worden geoptimaliseerd voor DTO’s en presentatie, terwijl het schrijfmodel is afgestemd op transactionele integriteit en domainerelen.

Koppel het samen als een Go-applicatie, niet als een schrijn

package app

import "your/module/internal/blog"

type Application struct {
	Commands Commands
	Queries  Queries
}

type Commands struct {
	PublishPost blog.PublishPostHandler
}

type Queries struct {
	LatestPosts   blog.LatestPostsHandler
	GetPostBySlug blog.GetPostBySlugHandler
}

Die vorm is niet toevallig. Three Dots Labs gebruiken een zeer vergelijkbaar patroon in Wild Workouts: een Application-type dat Commands en Queries exposeert, met concrete handlers die worden gekoppeld vanuit aparte app/command- en app/query-pakketten. Hun servicecompositiecode importeert die pakketten apart en construeert een enkel applicatieobject ervan. Het is een schone, Go-achtige manier om de grens duidelijk te maken zonder Framework Drama. Als je afhankelijkheidsgraaf complex wordt naarmate handlers vermenigvuldigen, behandelt Dependency Injection in Go Wire, Dig en constructor injection-patronen die natuurlijk combineren met deze handler-gebaseerde structuur.

Als je later asynchrone commando’s, cross-service events of een gedenoormaliseerde zoekindex nodig hebt, kun je ze vanuit deze basislijn toevoegen. Three Dots Labs presenteren asynchrone commandobussen en aparte querydatabases expliciet als latere optimalisaties, niet als het startpunt.

Go-bibliotheken die je moet kennen

Het Go-CQRS-ecosysteem is smaller dan het .NET-ecosysteem, wat eerlijk gezegd een zegen is. Je kunt de echte opties in een middag overzien en het adopteren van drie abstracties die je niet nodig hebt, vermijden.

Watermill

Watermill is het duidelijkste moderne keuze wanneer je CQRS plus messaging wilt. Zijn CQRS-component is een high-level API die je in staat stelt te werken met Go-structs in plaats van ruwe berichten, en zijn bouwstenen omvatten een EventBus, EventProcessor, CommandBus en CommandProcessor. De docs behandelen ook event handler-groepen voor geordende verwerking op gedeelde topics, een voorbeeld van een leesmodel en custom marshaling-metadata. Buiten de CQRS-laag ondersteunt Watermill een breed scala aan pub/sub back-ends, waaronder RabbitMQ, Kafka, NATS Jetstream, Redis Streams, Google Cloud Pub/Sub, SQL, HTTP en anderen. Pkg.go.dev markeert Watermill als productie-klaar met een stabiele publieke API sinds v1.0.0, en de huidige gepubliceerde moduleversie is v1.5.2, met GitHub die die release op 13 mei vermeldt.

commandBus, err := cqrs.NewCommandBusWithConfig(pub, cfg)
eventBus, err := cqrs.NewEventBusWithConfig(pub, cfg)
commandProcessor, err := cqrs.NewCommandProcessorWithConfig(router, cfg)
eventProcessor, err := cqrs.NewEventProcessorWithConfig(router, cfg)

Gebruik Watermill wanneer commando’s en events procesgrenzen moeten overschrijden, wanneer je wilt dat retries en herbezorgingssyntaxis first-class zijn, of wanneer je weet dat je “eenvoudige” service al halvereweg de event-driven realiteit is. Het nadeel is dat je nu gesprekken over brokers, topics, volgorde en idempotentie hebt, of je dat wilt of niet. Dat is geen gebrek aan Watermill. Dat is de kosten van de probleemdomein.

Event Horizon

Event Horizon is een CQRS- en event sourcing-toolkit voor Go. De maintainers beschrijven het als gebruikt in productiesystemen, maar noteren ook dat de API niet definitief is. De toolkit biedt helpers voor registratie van aggregates, commando’s en events, officiële event store-implementaties voor geheugen- en MongoDB-varianten, ondersteuning voor projecties en repositories, en voorbeelden die een op het outbox-patroon gebaseerde applicatie omvatten. De releasestream is nog steeds actief, met GitHub dat v0.17.0 op 16 juni toont en eerdere releases die functies toevoegen zoals snapshots, retry-able projecties, persistente commandoscheduling en het outbox-patroon.

eh.RegisterAggregate(func(id uuid.UUID) eh.Aggregate {
	return &InvoiceAggregate{ID: id}
})

eh.RegisterCommand(func() eh.Command {
	return &CreateInvoiceCommand{}
})

Event Horizon heeft het meeste zin wanneer event sourcing het doel is, niet een optionele toekomstige uitbreiding. Als je audit-vriendelijke streams, herhaalbare geschiedenis, projecties en een event-store-gecentreerd model wilt, is het een serieuze optie. Als je alleen schoner applicatieservices in een monolith wilt, is het waarschijnlijk meer machinerie dan je nodig hebt. De notitie “API is niet definitief” betekent ook dat je budget moet maken voor wat meer adaptatie over tijd dan je met Watermill zou doen.

Go-MediatR

Go-MediatR is geen volledig CQRS-framework, maar het is nuttig voor in-process CQRS. De README beschrijft het als een implementatie van het mediator-patroon dat wordt gebruikt met CQRS, met request/response-dispatch voor commando’s en queries, notification-dispatch voor events en pipeline-gedrag voor cross-cutting concerns. Het project heeft ook getagde releases, met GitHub dat v1.4.0 als de laatste release vermeldt en thread-safe handlerregistratie en concurrentie-gerelateerde verbeteringen benadrukt.

resp, err := mediatr.Send[*CreateProductCommand, *CreateProductResponse](ctx, cmd)
post, err := mediatr.Send[*GetPostBySlugQuery, *PostView](ctx, query)

Dit is een goede match als je handler-gebaseerde commando’s en queries wilt, maar geen broker, projectie-engine of event store. Het is vooral vriendelijk voor teams die komen van MediatR in .NET. De afweging is eveneens duidelijk: je moet nog steeds je eigen persistentie, leesmodelverversingsstrategie en out-of-process-integratieverhaal ontwerpen. Met andere woorden, het geeft je de applicatiegrens, niet de hele architectuur.

Oudere frameworks en referentiemateriaal

Er zijn oudere Go-CQRS-bibliotheken die nog steeds leerzaam zijn, maar ik zou ze als referentiemateriaal behandelen voordat ik ze als groene veldstandaarden zou behandelen.

jetbasrawi/go.cqrs beschrijft zichzelf als een Go-CQRS-referentie-implementatie met sample-applicaties gebaseerd op de principes van Greg Young. Echter, pkg.go.dev toont geen geldige go.mod, geen getagde versie en geen stabiele versie, terwijl GitHub geen releases toont en de pakketmetadata 7,4 jaar geleden werd gepubliceerd. Dat is nuttige geschiedenis, geen sterk signaal voor een verse productiadoptie in 2026.

andrewwebber/cqrs is vergelijkbaar: het biedt event sourcing, commando-uitgifte en -verwerking, event-publicatie en leesmodelgeneratie van gepubliceerde events, maar de pakketmetadata werd ook 7,4 jaar geleden gepubliceerd. Ik zou het absoluut lezen als je wilt begrijpen hoe eerdere Go-CQRS-bibliotheken het probleem benaderden. Ik zou voorzichtig zijn om het de basis van een nieuwe codebase te maken tenzij je blij bent om deeltijd-beheerder van je eigen architectuurstapeling te worden.

Een praktische Go-projectstructuur

Een typische Go-CQRS-structuur moet use-cases duidelijk maken, niet begraven onder generieke abstracties. Wild Workouts is een goede referentie hier. De repository scheidt bounded contexts onder internal, houdt commando’s en queries in afzonderlijke applicatiepakketten en koppelt ze aan een Application-type dat Commands en Queries exposeert. Servicecompositie trekt adapters, handlers en afhankelijkheden expliciet samen. De hier beschreven patronen komen overeen met de bredere richtlijnen in Go Project Structure: Practices & Patterns, die de bredere set van structuurkeuzen behandelt die teams tegenkomen naarmate Go-codebases groeien.

Een pragmatische structuur ziet er zo uit:

internal/
  blog/
    app/
      app.go
      command/
        publish_post.go
        unpublish_post.go
      query/
        get_post_by_slug.go
        latest_posts.go
    domain/
      post.go
      slug.go
    adapters/
      postgres/
        post_repository.go
        post_read_model.go
    ports/
      http/
        handler.go
    service/
      application.go

Deze structuur heeft een paar voordelen.

Ten eerste leven commando- en query-handlers dicht bij de use-cases die ze implementeren. Dat maakt het moeilijker om bedrijfsgedrag te verbergen in repositories of handlers die zijn vernoemd naar transportlagen. Three Dots Labs doen dit direct in Wild Workouts, waar app/command en app/query aparte pakketten zijn en het bovenliggende Application handlers groepeert op verantwoordelijkheid.

Ten tweede kan het domeinpakket zich richten op invarianten en gedrag, terwijl de queryzijde vrij is om DTO’s en projecties te retourneren. Dit komt overeen met de richtlijnen van Microsoft voor schrijf- en leesmodellen en vermijdt het veelvoorkomende CQRS-antipatroon waarbij de queryzijde terug wordt gedwongen door domeinobjecten alleen voor ideologische zuiverheid.

Ten derde schaleert deze structuur van de kleinste nuttige CQRS naar zwaardere varianten. Je kunt vandaag één PostgreSQL-database en twee repository-implementaties behouden, en later een zoekindex of event-driven leesprojectie toevoegen zonder de hele applicatievorm te hoeven herschrijven. Three Dots Labs beschrijven die progressie van basis-CQRS naar asynchrone commandobussen en aparte queryopslag expliciet pas wanneer het systeem ze nodig heeft.

Wanneer CQRS past en wanneer niet

CQRS heeft zin wanneer lees- en schrijfoperaties werkelijk verschillende problemen zijn. Microsoft beveelt het aan voor workloads waarbij lees- en schrijfmodellen onafhankelijke optimalisatie nodig hebben, waarbij meerdere gebruikers samenwerken aan dezelfde gegevens en waarbij duidelijke scheiding helpt met prestaties, schaalbaarheid en beveiliging. Microservices.io voegt een andere klassieke match toe: gedenoormaliseerde, high-performance weergaven gebouwd uit domeinevents of materialised projections. Three Dots Labs wijzen ook op complexe bedrijfslogica, onderhoudbaarheid en toekomstige uitbreiding naar asynchrone commando’s of gespecialiseerde leesopslag als sterke redenen om het in Go te adopteren.

In de praktijk betekent dat vaak systemen met rijke domainerelen, dure leesmodellen, rapportageweergaven die niet netjes mappen op aggregates, of microservices die events publiceren en projecties elders bouwen. In die context verschijnt het Saga-patroon voor gedistribueerde transacties vaak naast CQRS als het coördinatiemechanisme voor meerstapsbedrijfsoperaties die servicegrenzen overschrijden. Het past ook bij producten waarbij de schrijfzijde streng en auditabel moet zijn terwijl de leeszijde snel moet zijn en is gevormd voor UI- of API-consumptie. Als je al praat over projecties, replicas of het herbouwen van weergaven uit events, bevind je je waarschijnlijk al in CQRS-territorium, of je de label gebruikt of niet.

CQRS heeft geen zin wanneer je service een straightforward data-editor is. Fowler zegt openlijk dat CQRS voor de meeste systemen risicovol complexiteit toevoegt, en Three Dots Labs stellen dat eenvoudige CRUD-services die in wezen dezelfde gegevens ontvangen en retourneren, geen goede match zijn. In hun eigen Wild Workouts-voorbeeld gebruikt een eenvoudigere gebruikersservice geen Clean Architecture en CQRS omdat de patronen daar niet hun huur zouden betalen.

Dat is het deel dat plain in een technische blog waard is: CQRS is geen maturity-badge maar een bewuste trade-off, en het heeft alleen zin wanneer je echt nodig hebt wat het je geeft. Als je admin-panel rijen schrijft en dezelfde rijen terugleest, scheid dan het model niet alleen omdat je kunt. Als je commandohandlers voornamelijk “stel veld X op record Y” zijn, heb je geen CQRS-probleem. Je hebt een normale applicatie, en dat is volkomen respectabele software.

Afsluitende gedachten

De beste manier om CQRS in Go te implementeren is om te beginnen met de saaie versie. Scheid commandohandlers van queryhandlers. Laat commando’s de bedrijfsintentie modelleren. Laat queries leesmodellen retourneren. Behoud dezelfde database als dat alles is wat je nodig hebt. Voeg dan, pas wanneer het systeem je hand forceert, asynchrone bussen, projecties, aparte opslag of event sourcing toe. Die progressie is consistent met de waarschuwing van Fowler over complexiteit, de gefaseerde CQRS-richtlijnen van Microsoft en de pragmatische Go-voorbeelden van Three Dots Labs.

Als je een bibliotheek nodig hebt, is Watermill de sterkste all-round keuze voor message-driven CQRS in Go, is Event Horizon overtuigend wanneer event sourcing het zwaartepunt is, en is Go-MediatR een goede lichte touch wanneer je alleen in-process commando- en query-dispatch nodig hebt. Alles anders moet zijn plaats zeer zorgvuldig verdienen. Voor een bredere kaart van codestructuur, integratie en data-toegangspatronen in productie-Go-systemen, is de App Architecture guide een nuttige begeleider.

Dat, in het eind, is het meest Go-achtige antwoord op CQRS: gebruik het patroon, niet het kostuum.

Abonneren

Ontvang nieuwe berichten over systemen, infrastructuur en AI-engineering.