---
layout: 'page'
uri: '/skills/gk-domain-events'
position: 20
slug: 'skills-gk-domain-events'
parent: 'skills-domain'
navTitle: 'gk-domain-events'
title: 'GK — Domain events'
description: 'Vyhlášení "stalo se X" tak, aby na to reagoval kdokoli další, aniž by command handler musel vědět kdo — per-request sběrač, primitivní payloady, synchronní rozeslání až po commitu. Use when chceš po úspěšném commandu spustit vedlejší efekt (notifikace, indexace, follow-up) bez toho, aby ho command handler znal.'
name: 'gk-domain-events'
---
# GK — Domain events
Command handler vyhlásí, že se něco stalo (`UserCreated`), a neřeší kdo na to
reaguje. Eventy se sesbírají v rámci jednoho commandu a rozešlou se **až po
úspěšném commitu** — když command spadne, zahodí se.
## What & when
- Sáhni sem, když po nějakém commandu (`CreateUser`, …) chceš spustit vedlejší
efekt — poslat welcome mail, zaindexovat, upozornit jiný kontext — a nechceš,
aby command handler znal mailer/indexer/notifier.
- NEtýká se: práce, co musí přežít restart/crash nebo trvá dlouho (externí API,
mail) → patří do durable enginu (fire-and-forget run nebo durable run), ne do synchronního
event handleru (viz `/gk-runs`).
Audit logu — ten má vlastní cestu (`AuditCollector`), ne eventy.
## For non-tech / juniors
Představ si event jako **veřejné oznámení na nástěnce**: „byl založen uživatel".
Ten, kdo oznámení vyvěsil (command handler), neřeší, kdo si ho přečte. Kdokoli
další (odesílač mailů, …) se může samostatně přihlásit „když uvidíš tohle
oznámení, udělej tamto". Klíčové: oznámení se rozešle **až když je změna
opravdu uložená v databázi**. Když uložení selže, oznámení se zahodí — nikdy
nepošleme welcome mail uživateli, který ve skutečnosti nevznikl.
## How it works
**`DomainEvent`** je jednoduchý interface (`app/domain/shared/event.go`):
`EventName() string` + `OccurredAt() time.Time`. Event je čistá data struct
**jen z primitivů** (`string`, `time.Time`), aby šel serializovat a aby ho mohl
přečíst i cizí kontext bez importu.
Reálný (a zatím jediný) event — `app/domain/user/user_created.go`:
```go
type UserCreated struct {
UserID, Nickname, Email, Role string
Timestamp time.Time
}
func (e UserCreated) EventName() string { return "user.created" }
func (e UserCreated) OccurredAt() time.Time { return e.Timestamp }
```
**`EventCollector`** (`app/domain/shared/event.go`) sbírá eventy v rámci jednoho
commandu. Je thread-safe (mutex) a má příznak `forbidden`. Instance je
**per-request** — žádný singleton, takže se paralelní requesty nepřelévají.
**Tok přes `CommandBus`** (chain single-sourced v `busmw.CommandChain`, `app/application/bus/middleware/base.go`; DI ho volá z `provideCommandBus`):
```
… → DispatchEventsMiddleware → TransactionMiddleware → handler
```
`DispatchEventsMiddleware` (`app/application/bus/middleware/events.go`) **obaluje**
`TransactionMiddleware` (je vně). Pořadí dělá tu záruku:
1. `DispatchEventsMiddleware` vytvoří per-request collector a uloží ho do `ctx`
(`shared.ContextWithEventCollector`).
2. `TransactionMiddleware` otevře transakci.
3. Handler volá `shared.EventCollectorFromContext(ctx).Collect(event)` — typicky
hned po úspěšném uložení (viz sdílené create tělo `userwrite.create`, do kterého
ústí obě vstupní brány `userwrite.Create` i `userwrite.CreateSuperAdmin`, aby
CreateUser, CreateSuperAdmin i CreatePlatformUser emitovaly stejně).
4. Transakce se potvrdí (commit), nebo se při chybě vrátí zpět (rollback).
5. Když `next` vrátí chybu, middleware ji jen propaguje a **flush přeskočí**
(eventy zmizí). Když je commit OK, `collector.Flush()` vrátí eventy a každý se
pošle přes `eventBus.Dispatch(ctx, event)` — **synchronně, v request goroutině**.
**`EventBus.Dispatch`** (`app/application/bus/event.go`) najde handlery podle
`EventName()` a volá je **sériově, v pořadí registrace**. Před voláním nainstaluje
„forbidden" collector (`ContextWithoutEventCollector`), takže když event handler
zkusí `Collect`, runtime **panicne** s jasnou hláškou — kaskáda eventů není
podporovaná.
**Registrace** je jediné místo — `provideEventHandlers()` v
`app/infrastructure/di/container_provider.go` (stejný slice-list pattern jako
permissions / scheduler joby / run handlery). Dnes vrací prázdný slice; složka
`app/application/user/event/` je zatím prázdná (`.gitkeep`) — **žádný event
handler ještě není nasazený**, jen se eventy sbírají a logují.
## Recipe
Cíl: po `CreateUser` spustit nový vedlejší efekt na event `user.created`.
1. **Event** už existuje (`app/domain/user/user_created.go`) a emituje se
(`userwrite.go` — sdílené create tělo, na které deleguje `create_user.go`). Pro nový event přidej struct jen z primitivů + `EventName()`
+ `OccurredAt()` do `domain/<kontext>/` a `Collect` ho v handleru **až po
úspěšném zápisu**.
2. **Handler** napiš do `app/application/<kontext>/event/`. Signatura:
`func (h *XHandler) Handle(ctx context.Context, event shared.DomainEvent) error`.
Uvnitř si event přetypuj: `e := event.(user.UserCreated)`.
3. **Zaregistruj** ho v `provideEventHandlers()`:
```go
{Event: "user.created", Handler: welcome.Handle},
```
(přidej i `wire.Build` provider pro samotný handler, pokud má závislosti).
4. `make di` → `make arch-check` → `make test`.
## Invariants & pitfalls
- **Handler běží PO commitu, v žádné transakci.** `DispatchEvents` obaluje `Transaction`, takže
než event handler dostane slovo, transakce commandu je už uzavřená (commitnutá a z ctx pryč) —
handler tedy v žádné transakci neběží a jeho případné zápisy jsou samostatné (každý sám za sebe).
Běží synchronně v request goroutině, takže pro těžkou návaznou práci **zařaď durable run**
([[gk-runs]] — fire-and-forget run nebo dlouhý run), ať neblokuješ response. Kdy co →
`docs/framework/background/overview.md`.
- **Eventy = jen primitivy.** Nikdy nedávej do eventu entity ani value objects —
musí jít serializovat a číst z cizího kontextu bez importu.
- **`Collect` až po úspěšném zápisu**, ne před. Když handler vrátí chybu / commit
selže, eventy se zahodí — ale logika musí být „nejdřív ulož, pak vyhlas".
- **Dispatch je synchronní** v request goroutině → pomalý handler prodlouží HTTP
response. Pro mail / externí API použij durable engine (`/gk-runs`), ne event handler.
- **Žádná kaskáda.** `Collect` z event handleru **panicne** (forbidden collector
nastavený v `EventBus.Dispatch`). Pro follow-up async práci sáhni po
`shared.RunDispatcherFromContext(ctx).Enqueue(...)`.
- **Handler nemůže odvolat command.** Když selže, command už commitnul; vrácená chyba se jen zaloguje a zahodí (`LoggingMiddleware` na EventBusu; panika projde `RecoveryMiddleware` — zaloguje se a reportuje do Sentry), uživatel dostal 2xx.
- **Mimo bus** (přímé volání handleru v testech) vrátí `EventCollectorFromContext`
*jednorázový* sběrač — `Collect` projde, ale nikam to nejde. CLI commandy teď
jedou přes `SystemCommandBus` (má DispatchEvents), takže event z `create-user` se
po commitu rozešle; seeder ale eventy nesbírá (staví entity přímo) — žádný welcome
mail pro seedovaného admina.
- **Registruj jen v `provideEventHandlers()`** během DI initu (single-goroutine).
`EventBus.Dispatch` čte mapu handlerů bez zámku — registrace po prvním dispatchu
by byl data race.
## Related
- Sousední skills: `/gk-runs` (perzistentní durable engine — sem patří mail/externí
volání jako fire-and-forget run), `/gk-config` (Wire DI registrace handlerů)
- Kód: `app/domain/shared/event.go` (interface + collector),
`app/application/bus/middleware/events.go` (dispatch po commitu),
`app/application/bus/event.go` (`EventBus`, `Register`, `Dispatch`),
`app/domain/user/user_created.go` (vzorový event),
`app/application/userwrite/userwrite.go` (emit site — sdílené create tělo; volá ho `create_user.go` i CLI create-superadmin),
`app/infrastructure/di/container_provider.go` (`provideEventHandlers` / `provideEventBus`)
---
[← gk-entities](/skills/gk-entities.md) | [gk-errors →](/skills/gk-errors.md)