Kapitola 12 · Vzory · CQRS v Symfony 8

CQRS v Symfony 8

Implementace CQRS (Command Query Responsibility Segregation) v Symfony 8 s využitím DDD principů – oddělení operací čtení a zápisu, optimalizace read modelů, řešení eventual consistency a stavba škálovatelných aplikací.

Autor M. Katuščák
Doba čtení ≈ 35 min
Náročnost pokročilá
Publikováno · Aktualizováno ·
Obsah kapitoly

12.01 Co je CQRS?#

CQRS vychází z prostého pozorování: model, který slouží k zápisu dat, nemusí být tentýž model, který slouží k jejich čtení. CQRS (Command Query Responsibility Segregation) tento princip přenáší z jednotlivé metody na celý model – popsal jej Greg Young [1]. Kořeny sahají ke Command-Query Separation (CQS) Bertranda Meyera [2]. Young v CQRS Documents dodává, že první roky se o vzoru mluvilo jako o rozšíření CQS na vyšší úrovni. Tuto formulaci sám označuje za nepřesnou – po letech záměn obou pojmů se CQRS ustálil jako samostatný vzor, ne jako varianta staršího pravidla.

V tradičních aplikacích používáme jednu entitu (např. Doctrine ORM entity) pro obojí – vytváříme objednávku i zobrazujeme seznam objednávek přes tentýž objekt Order. CQRS tuto zodpovědnost explicitně rozděluje do dvou oddělených modelů, z nichž každý nese vlastní úkol a vlastní optimalizační profil.

CQRS se často kombinuje s Event Sourcing, což je vzor, který místo aktuálního stavu ukládá historii změn jako sekvenci událostí. Tyto dva vzory jsou však nezávislé. CQRS lze plnohodnotně implementovat s klasickou Doctrine ORM persistencí na write straně a denormalizovanými tabulkami na straně čtení, aniž by se sahalo po Event Sourcingu.

12.02 CQS vs. CQRS – kde je hranice?#

Bertrand Meyer formuloval princip Command-Query Separation (CQS) jako pravidlo na úrovni metod: každá metoda by měla buď měnit stav (command), nebo vracet hodnotu (query), ale nikdy obojí. CQS je návrhové pravidlo pro rozhraní tříd.

Greg Young tutéž myšlenku posunul z jednotlivé metody na model: CQRS není pravidlo pro metody, ale rozdělení jednoho doménového modelu na dva. Každý má vlastní sadu tříd, vlastní úložiště a vlastní optimalizační profil. Sám Young přitom zdůrazňuje, že CQRS není architektura, nýbrž architektonický vzor, a že popisuje něco uvnitř jediného systému nebo komponenty – ne uspořádání celé aplikace [5].

V praxi se CQS přirozeně stává výchozím bodem pro CQRS. Pokud dodržujete CQS na úrovni metod, zjistíte, že metody měnící stav (command methods) potřebují výrazně jiná data než ty, které jej čtou (query methods). CQRS toto pozorování formalizuje rozdělením do dvou explicitních modelů.

12.03 Výhody CQRS#

CQRS přináší architektonické výhody zejména u aplikací s netriviální doménovou logikou a odlišnými požadavky na čtení a zápis.

První výhodou je oddělení odpovědností. Write model nese doménovou logiku, validaci invariantů a konzistenci dat; read straně zbývá jediný úkol – dostat data v podobě, jakou vyžaduje obrazovka. Každý model obsahuje jen to, co ke své práci potřebuje, a lze ho optimalizovat nezávisle: na straně zápisu normalizované relační schéma a Doctrine ORM entity s bohatou doménovou logikou, na straně čtení denormalizovaná tabulka, Elasticsearch index nebo Redis cache – cokoli, co nejlépe vyhovuje konkrétním dotazům. Z téhož oddělení plyne i volnost při evoluci: read model jde kdykoli přebudovat (rebuild projekcí), doplnit o nový read model pro nový use case nebo změnit strukturu dotazu – bez jakéhokoli dopadu na write model a doménovou logiku.

Dvě další výhody:

  • Škálovatelnost – Young uvádí, že v systémech, a zvlášť ve webových, odbaví dotazovací strana běžně o dva a více řádů víc operací než strana zápisu [1]. CQRS umožňuje škálovat read stranu nezávisle (repliky, cache, CDN), bez dopadu na write stranu.
  • Testovatelnost – Command handlers se testují jako čistě doménová logika (given state → when command → then events/state). U query handlerů se ověřuje jen správnost vrácených dat. Žádné propletení obou odpovědností v jedné testovací sadě. Viz kapitola Testování DDD kódu.

12.04 Výzvy a omezení CQRS#

CQRS má své limity. Kompromisy, které přináší, je lepší znát ještě před zavedením.

Místo jednoho modelu existují dva (nebo více) a každý command či query vyžaduje vlastní třídu, handler a často i vlastní datovou strukturu – tam, kde by v CRUD stačila jedna třída, jich vznikne několik. Při oddělených úložištích se přidává synchronizace: read model se musí aktualizovat po každé změně write modelu, aby se s ním nerozešel. Selhání propagace (výpadek fronty, chyba projektoru) vede k divergenci modelů.

Sem patří i eventual consistency. Mezi zápisem a aktualizací read modelu vzniká okno, kdy uživatel po odeslání formuláře vidí „starou“ verzi dat. Vzory pro UI popisuje sekce Eventual Consistency.

Poslední cenou jsou nároky na zaučení týmu. CQRS vyžaduje změnu myšlení oproti tradičnímu přístupu, kde jeden model pokrývá všechny operace. Vývojáři musejí porozumět konceptům jako message bus, eventual consistency, idempotence handlerů a read model projekce.

Technická kritéria přitom nejsou jedinou osou rozhodování. Udi Dahan, který vzor pomáhal popularizovat, staví na kolaborativnosti domény: CQRS dává smysl tam, kde více aktérů mění tatáž data podle kontextově závislých pravidel [6]. Nákupní košík mezi takové domény nepatří, protože nikdo neupravuje košík někoho jiného. Podle Dahana proto pro CQRS nekandiduje ani tehdy, když je poměr čtení k zápisu extrémní. Ke stejné opatrnosti vede Martin Fowler: vzor se rozhoduje per bounded context, ne pro celý systém [7].

12.05 Symfony Messenger jako základ CQRS#

Message bus není pro CQRS podmínkou. Oddělené command a query třídy volané přímo z controlleru jsou plnohodnotná úroveň 1 a v malé aplikaci nic dalšího nepotřebují. Sběrnice se vyplatí ve chvíli, kdy kolem zpracování přibývá společná infrastruktura – transakce, validace, logování, odložené vykonání. V Symfony tuto roli plní Messenger. Dedikované PHP knihovny z let 2014–2018 mezitím skončily (broadway/broadway je archivovaný) nebo roky nedostaly commit (prooph/service-bus, SimpleBus). Volba se tím zúžila na Messenger, nebo vlastní tenkou vrstvu nad kontejnerem.

Pro CQRS je na Messengeru podstatná schopnost definovat více message busů – jeden pro příkazy, jeden pro dotazy a jeden pro doménové události. Každý bus má vlastní sadu middleware, vlastní transport a vlastní strategii zpracování. Dokumentace Symfony k tomu dodává podmínku, kterou se vyplatí brát vážně: jeden bus je dobrý výchozí stav a další se přidává tehdy, když potřebuje jiný middleware stack, ne proto, že to nějaký vzor doporučuje [8]. Konfigurace níže tuto podmínku splňuje – command bus obaluje handler do transakce, query bus ne.

FIG. 12.5-A Symfony Messenger jako CQRS bus

Konfigurace definuje dva transporty: async pro zpracování přes frontu a sync pro okamžité vykonání v témže procesu. Busy jsou tři. command.bus pro příkazy má doctrine_transaction middleware, tedy automatickou transakci kolem handleru. query.bus obsahuje pouze validaci. event.bus slouží doménovým událostem a liší se v jednom podstatném bodě: příkaz bez handleru je chyba, událost bez posluchače legitimní stav. Proto allow_no_handlers: true – bez něj Messenger vyhodí NoHandlerForMessageException u každé události, kterou zatím nikdo neodebírá.

12.06 Implementace Commands#

Commands v CQRS jsou příkazy, které mění stav systému. V Symfony 8 se implementují jako jednoduché PHP třídy – immutabilní datové objekty (DTO), které nesou veškerá data potřebná pro vykonání operace. Command sám o sobě neobsahuje žádnou doménovou logiku; je to pouhý přepravní kontejner dat.

Dobře navržený command má několik vlastností:

  • Je immutabilní (readonly properties) – po vytvoření se nemění.
  • Obsahuje validační atributy – díky middleware validation na command busu se command validuje ještě před předáním handleru.
  • Pojmenování vyjadřuje záměrRegisterUser, PlaceOrder, CancelSubscription. Ne SaveUser nebo UpdateOrder.
  • Pracuje typicky s primitivními typy (string, int, float) nebo serializovatelnými hodnotovými objekty (např. OrderId, Money). Command musí jít bezpečně přenést přes asynchronní kanál.

Záměr se přitom nebere odnikud. U Younga stojí před CQRS task-based UI – rozhraní složené z úloh („Změnit doručovací adresu“, „Stornovat objednávku“), ne formulář nad entitou s tlačítkem Uložit. Formulář mapovaný na entitu vyprodukuje jediný command UpdateOrder a informace o tom, co uživatel vlastně chtěl, se ztratí ještě před vstupem do domény. Úlohy v UI přitom obvykle odpovídají doménovým událostem, které tým našel při Event Stormingu.

Validační atributy na commandu a doménová pravidla v handleru řeší dvě různé věci. Dahan odděluje validaci, která je kontextově nezávislá (je e-mail e-mailem, má heslo dost znaků), od business rules, které kontextu podléhají (tento e-mail už někdo použil, zákazník vyčerpal denní limit). Formálně validní command proto může doménově selhat, protože se mezitím změnily podmínky. Middleware validation doménovou kontrolu nenahrazuje, jen odfiltruje zprávy, které nedávají smysl ani po formální stránce.

12.07 Implementace Queries#

Query se od commandu liší směrem toku dat: nemění stav systému, jen čte. Implementace vypadá podobně – immutabilní DTO třída – s jedním rozdílem: query vždy vrací hodnotu, kterou handler předá přes HandledStamp.

Dotaz nese jediné pole: ID uživatele, jehož profil chceme získat. Nevalidní UUID odmítne validation middleware ještě před zpracováním.

12.08 Implementace Handlers#

Handler je místo, kde se zpráva potká s logikou. V Symfony 8 jde o třídu s atributem AsMessageHandler a metodou __invoke(). Symfony Messenger automaticky spojí handler s jeho command/query podle type-hintu parametru.

Command handler a query handler mají odlišnou odpovědnost:

  • Command handler – Načte agregát z repozitáře, zavolá na něm doménovou metodu (která validuje invarianty) a uloží změny. Může emitovat doménové události. Pracuje s doménovým modelem (entity, value objects, repozitáře).
  • Query handler – Čte data z optimalizovaného zdroje (denormalizovaná tabulka, Elasticsearch, cache) a vrací je jako ViewModel. Nepracuje s doménovým modelem – obchází ho záměrně, protože doménový model není optimalizovaný pro čtení.

Rozdíl je vidět přímo v závislostech: command handler pracuje s doménovým modelem (UserRepository, User entita, value objects), zatímco query handler sahá do read repozitáře (UserProfileReadRepository), který vrací přímo ViewModel – jednoduchou datovou strukturu optimalizovanou pro prezentaci. Query handler neprochází přes doménový model.

12.09 ViewModely a Read Modely#

ViewModel (nebo Read Model) je datová struktura navržená výhradně pro potřeby konkrétního dotazu nebo obrazovky. Na rozdíl od doménové entity neobsahuje žádnou doménovou logiku – je to čistě prezentační objekt. Zatímco doménová entita User chrání invarianty a zapouzdřuje chování, ViewModel UserProfileViewModel obsahuje přesně ta data, která potřebuje šablona nebo API endpoint.

ViewModel často obsahuje data z více agregátů – v příkladu výše kombinuje údaje o uživateli s počtem objednávek a členskou úrovní. Sestavení téhož pohledu přes doménový model by vyžadovalo načtení uživatele, jeho objednávek a propočet úrovně – pomalé a porušující hranice agregátů. Read model tato data drží připravená v denormalizované podobě.

12.10 Implementace Command a Query Buses#

Zbývá dopravit příkazy a dotazy ke správnému handleru. V Symfony 8 se pro injektování busu používá named autowiring – názvy parametrů v konstruktoru musejí odpovídat konfiguraci v messenger.yaml:

V těchto příkladech Symfony přiřadí bus podle názvu parametru v konstruktoru: klíč command.bus z konfigurace buses se namapuje na $commandBus.

Tím končí popis základní infrastruktury CQRS – příkazů, dotazů, handlerů a busů. Následující sekce se věnují pokročilejším aspektům: optimalizaci read strany pro konkrétní dotazy, eventual consistency a provozním problémům v asynchronním prostředí.

12.11 Optimalizace Read Modelů#

Read strana má volnou ruku ve výběru struktury. Write model drží normalizaci kvůli konzistenci dat; read model může jít opačným směrem – denormalizovat data přesně do tvaru, který obrazovka nebo API endpoint očekává.

Strategie optimalizace read modelů

Denormalizované tabulky jako read model

Nejrozšířenější strategií v praxi je denormalizovaná tabulka, která drží data předpočítaná pro jedinou obrazovku či endpoint. Tabulka se aktualizuje asynchronně přes doménové události.

Kdo doménové události odešle

Projektor výše předpokládá, že mu události OrderPlaced či OrderShipped někdo doručí. V nejjednodušší podobě je po flush() vyzvedne aplikační vrstva z agregátu metodou releaseEvents() a dispatchne je na event bus – celý mechanismus popisuje sekce Agregát a doménové události: lifecycle. Pro vývoj a méně kritické projekce tato synchronní cesta stačí.

Má ale slabé místo: dispatch po flushi není atomický. Spadne-li proces mezi commitem transakce a odesláním do fronty, událost se ztratí a projekce tiše diverguje od write modelu. Produkční řešení ukládá události do outbox tabulky ve stejné transakci jako agregát a do fronty je publikuje samostatný relay proces – podrobně v kapitole Outbox Pattern.

Rebuild projekcí

CQRS s asynchronními projekcemi umožňuje kompletní rebuild read modelu. Pokud se změní struktura denormalizované tabulky (nový sloupec, jiný formát dat), stačí:

  1. Vytvořit novou verzi projekční tabulky.
  2. Přehrát všechny relevantní události přes projektor.
  3. Přepnout read dotazy na novou tabulku.
  4. Smazat starou tabulku.

Tento přístup je realizovatelný pouze tehdy, jsou-li zdrojové události stále dostupné (v Event Store nebo v message logu). Bez Event Sourcingu je rebuild projekcí možný, ale musíte mít alternativní zdroj dat (např. change data capture z write databáze).

12.12 Eventual Consistency v praxi#

Eventual consistency je nejčastějším zdrojem nejistoty při zavádění CQRS. Při asynchronní propagaci změn z write strany na read stranu existuje časové okno (typicky milisekundy až jednotky sekund), kdy read model ještě neodráží poslední zápis. Uživatel odešle formulář, dostane potvrzení o úspěchu, ale seznam na další stránce ještě nezobrazuje nový záznam.

Nejde o bug, ale o vlastnost distribuované architektury. Následující diagram zachycuje celý datový tok – od zápisu přes asynchronní propagaci až po čtení – a zvýrazňuje okno, ve kterém k eventual consistency dochází:

FIG. 12.12-A Eventual consistency v CQRS toku

Konkrétnější časový pohled na to, kdy uživatel vidí 404 navzdory tomu, že command proběhl úspěšně, je v následující sekvenci:

FIG. 12.12-B Okno zastaralosti – kdy GET vrátí 404 po úspěšném POST

Existuje několik osvědčených vzorů, jak eventual consistency v UI řešit:

Strategie řešení v UI

Read-your-writes na úrovni HTTP

Strategie z tabulky výše řeší vnímání uživatele v prohlížeči. API klienti potřebují tvrdší záruku: „přečti si, co jsi právě zapsal“ (read-your-writes). Docílit jí lze předáním pozice zápisu – odpověď na command nese číslo verze agregátu nebo offset, na který se projekce musí dostat. Klient hodnotu pošle s následujícím dotazem, typicky v hlavičce.

Čtecí endpoint porovná aktuální pozici projekce s požadovanou. Pokud projekce ještě zaostává, krátce počká (desítky až stovky milisekund) a porovnání zopakuje. Po vypršení limitu vrátí klientovi signál k opakování – 202 Accepted s hlavičkou Retry-After, načež klient data po uvedené pauze načte znovu (refetch). Stavový kód 304 se k tomu nehodí: znamená „vaše cache je platná“, ne „data ještě nejsou“.

Vzor se vyplatí jen na cestách, kde klient bezprostředně po zápisu čte tatáž data. Plošné nasazení by čtecí stranu zatížilo čekáním, které většina dotazů nepotřebuje.

12.13 Asynchronní zpracování#

Asynchronní zpracování příkazů je přirozeným pokračováním oddělené command strany. V Symfony 8 se konfiguruje přes transporty v Messenger komponentě. Příkaz označený pro asynchronní transport je při dispatchi serializován a zařazen do fronty; Messenger worker jej později vyzvedne a předá handleru.

Tato konfigurace směruje příkazy pro odesílání e-mailů a generování reportů na asynchronní transport s retry strategií (3 pokusy s exponenciálním backoffem). Klíč jitter k vypočtenému zpoždění přidá náhodný rozptyl a rozprostře opakování v čase; bez něj se po výpadku vrátí všechny zprávy naráz. Celou strategii lze nahradit vlastní implementací RetryStrategyInterface přes klíč service. Pro kritické události definuje konfigurace samostatný transport async_priority_high s vlastní frontou – Messenger worker pro tuto frontu může běžet s vyšší prioritou nebo na dedikovaném serveru.

Spolehlivé předání doménových událostí do fronty, atomické se zápisem agregátu, zajišťuje Outbox Pattern.

Transport si zpráva může nést i sama: atribut #[AsMessage(transport: 'async')] nad třídou příkazu nebo události nahradí odpovídající řádek v sekci routing:. Volba je věcí zvyku. YAML drží směrování na jednom místě, atribut ho má u zprávy.

12.14 Zpracování chyb a Dead Letter Queue#

Zpracování chyb se v asynchronním prostředí podstatně liší od synchronního světa. Při synchronním dispatchi výjimka probublá přímo do controlleru a uživatel vidí chybovou hlášku. Při asynchronním dispatchi je zpráva ve frontě – pokud handler selže, uživatel o tom neví a zpráva musí být zpracována znovu.

Retry strategie

Symfony Messenger podporuje automatické opakování zpráv, které selhaly. Konfigurace retry_strategy na transportu definuje, kolikrát a s jakým zpožděním se handler znovu zavolá:

  • max_retries: 3 – Maximální počet opakování.
  • delay: 1000 – Zpoždění prvního opakování (v ms).
  • multiplier: 2 – Exponenciální backoff: 1s → 2s → 4s.
  • max_delay: 60000 – Maximální zpoždění (60 sekund).
  • jitter: 0.1 – Náhodný rozptyl zpoždění, hodnota 0 až 1 (výchozí 0.1).

Failed transport (Dead Letter Queue)

Když selžou všechny pokusy o retry, Messenger zprávu přesune na failed transport (dead letter queue). Zprávy na failed transportu čekají na manuální zpracování – vývojář je může prozkoumat, opravit příčinu chyby a znovu odeslat.

Klíč failure_transport funguje globálně i u jednotlivého transportu. Projekce tak mohou mít vlastní dead letter frontu oddělenou od e-mailů a reportů, což usnadní jak monitoring, tak hromadné přehrání po opravě projektoru.

12.15 Middleware v CQRS#

Middleware v Symfony Messenger tvoří řetěz komponent kolem handleru – zachycuje zprávu před zpracováním a po něm. Tudy do dispatch cyklu vstupuje validace, logování, transakce nebo autorizace, aniž by se musel měnit handler.

Vestavěné middleware validation a doctrine_transaction se objevily v dřívější konfiguraci. Pro pokročilejší scénáře si můžete vytvořit vlastní middleware:

Na pořadí middleware záleží: v příkladu výše se logování provede jako první (zachytí i validační chyby), následuje validace (odmítne nevalidní command ještě před zahájením transakce) a nakonec doctrine_transaction (obalí handler do DB transakce).

12.16 Testování CQRS#

CQRS usnadňuje testování. Command handlers, query handlers a projektory jsou izolované komponenty s jasně definovanými vstupy a výstupy. Testovací strategie se liší podle testované komponenty:

Testování command handlerů

Command handler se testuje jako unit test s mocknutým repozitářem. Ověřujete, že handler správně validuje invarianty, volá doménový model a ukládá změny:

Testování query handlerů

Query handler se testuje na správnost mapování dat z read repozitáře na ViewModel. Pro integrační testy s reálnou databází můžete ověřit i správnost SQL dotazů:

Testování projektorů

Projektory se nejlépe testují jako integrační testy s reálnou databází. Ověřujete, že po zpracování sekvence událostí read model obsahuje očekávaná data:

Kompletnější přehled testovacích strategií pro DDD kód – včetně testování agregátů, value objects a doménových služeb – najdete v kapitole Testování DDD kódu.

12.17 Saga / Process Manager#

Při použití CQRS s více Bounded Contexts vzniká potřeba koordinovat dlouhotrvající procesy napříč kontexty. Vzor Saga – v orchestrované podobě označovaný Process Manager – naslouchá doménovým událostem a podle nich odesílá příkazy, čímž propojuje command a event stranu CQRS do ucelených doménových procesů.

Podrobný výklad ság – včetně implementace v Symfony Messenger, kompenzačních strategií a testování – najdete v kapitole Ságy a Process Managery.

Časté otázky

Co je CQRS?

CQRS (Command Query Responsibility Segregation) je architektonický vzor, který rozděluje aplikaci na dva oddělené modely: write model pro změny stavu a read model pro dotazy. Write model se soustředí na doménovou logiku a validaci invariantů, read model na rychlou prezentaci dat uživateli. Každý model lze nezávisle optimalizovat i škálovat. Popsal jej Greg Young; kořeny sahají ke staršímu pravidlu CQS Bertranda Meyera, sám Young ale označuje formulaci „CQRS je rozšíření CQS“ za nepřesnou a mluví o samostatném vzoru. Viz úvodní sekce.

Jaký je rozdíl mezi CQS a CQRS?

CQS (Command Query Separation) je návrhové pravidlo na úrovni metod – každá metoda by měla buď měnit stav, nebo vracet hodnotu, ne obojí. CQRS (Command Query Responsibility Segregation) posouvá tutéž myšlenku z metody na model: místo jednoho doménového modelu vznikají dva oddělené, každý s vlastními třídami, úložištěm i optimalizačním profilem. CQS je tedy princip ve třídě, CQRS rozhodnutí o podobě modelu uvnitř systému. Více v sekci CQS vs. CQRS.

Kdy se vyplatí CQRS nasadit?

CQRS přináší hodnotu v aplikacích, kde se požadavky na zápis a čtení výrazně liší – například doménově bohatý write model s mnoha invarianty proti výrazně převažujícím dotazům, které potřebují denormalizovaná data. Uplatní se také tam, kde má čtení nezávislý škálovací profil (repliky, cache, full-text vyhledávání) nebo kde je hodnota v odděleném auditu změn. U jednoduchých CRUD operací zvyšuje počet tříd bez odpovídajícího přínosu. Podrobný rozbor ve Výhodách CQRS a Výzvách a omezeních.

Musím použít Event Sourcing, když používám CQRS?

Ne. CQRS a Event Sourcing jsou nezávislé vzory, které se často kombinují, ale každý z nich lze zavést samostatně. CQRS lze plnohodnotně implementovat s klasickou Doctrine ORM persistencí na write straně a denormalizovanými SQL tabulkami na read straně. Event Sourcing lze naopak zavést i bez CQRS – byť kombinace obou je v praxi běžná, protože si vzájemně prospívají. Rozbor vztahu obou vzorů v sekci Co je CQRS.

Potřebuje CQRS frontu nebo druhou databázi?

Ne. Message bus, asynchronní transport i oddělené úložiště jsou volby, ne součást vzoru. Greg Young popisuje read stranu jako tenkou vrstvu nad toutéž databází, která promítá řádky rovnou do DTO; Azure Architecture Center uvádí, že posílání zpráv není pro CQRS podmínkou. Nejčastěji nasazovaná podoba CQRS je proto ta nejjednodušší: oddělené command a query handlery nad jednou databází. Viz Tři mýty o CQRS.

Jak se CQRS implementuje v Symfony?

Základním stavebním kamenem je komponenta Symfony Messenger, která funguje jako sběrnice pro příkazy a dotazy. Pro CQRS se obvykle definují dvě až tři oddělené sběrnice (command.bus, query.bus a pro doménové události event.bus), každá s vlastní sadou handler tříd a middleware. Dokumentace Symfony přitom doporučuje přidávat další sběrnici jen tehdy, když potřebuje jiný middleware stack. Příkazy mění stav a nevracejí data; dotazy vracejí ViewModely (read modely) a stav nemění. Asynchronní zpracování lze zapnout přes transport, což umožňuje dlouhé operace vytáhnout z request-response cyklu. Více v sekci Symfony Messenger jako základ CQRS.