Középhaladó Olvasási idő: ~7 perc

Event-driven architektúra

@EventListener, domain events, ApplicationEventPublisher

Event-driven architektúra

A Spring-es eventek akkor igazán hasznosak, ha világosan elválasztod a domain történéseket a technikai megvalósítástól és tudod, mikor maradjanak applicationön belül.

1. Definíció / Definition

Mi ez? / What is it?

Az event-driven gondolkodás lényege, hogy a rendszer állapotváltozásokat és fontos üzleti történéseket eseményekként modellez. Spring alatt ez legegyszerűbben ApplicationEventPublisher, @EventListener és @TransactionalEventListener segítségével jelenik meg.

Miért létezik? / Why does it exist?

Azért, hogy a komponensek ne tudjanak egymás minden mellékhatásáról közvetlenül. Egy művelet publikál egy eseményt, más részek pedig reagálhatnak rá. Így a fő üzleti use case tisztább marad, és az utólag hozzáadott reakciók kevésbé törik szét a kódot.

Hol helyezkedik el? / Where does it fit?

A Spring application eventek tipikusan egy JVM-en belüli, in-process mechanizmus. Nem helyettesítik automatikusan a Kafkát vagy RabbitMQ-t. Kiválóak belső decouplingra, domain event propagációra, audit triggerre vagy mellékhatások elválasztására.

2. Alapfogalmak / Core Concepts

2.1 Event publisher és listener

A publisher publishEvent(...) hívással jelez egy történést. A listener @EventListener annotációval reagál.

Tipikus in-process event flow:

  1. Az application service meghívja a publishEvent(OrderPlaced) metódust.
  2. A Spring minden illeszkedő listenernek továbbítja az eseményt.
  3. Az egyes listenerek külön mellékhatásokat hajthatnak végre.

2.2 Synchronous vs asynchronous

Alapból a Spring event listener synchronous. Ez fontos, mert a publisher threadjében fut, így:

  • lassíthatja a kérés feldolgozását;
  • exception visszabukhat;
  • tranzakciós kontextust is érintheti.

Ha @Async-szel futtatod, a listener leválik a hívóról, de cserébe jön a concurrency, retry és observability kérdés.

2.3 Transactional eventek

@TransactionalEventListener akkor hasznos, ha csak sikeres commit után akarsz reagálni.

Fázis Jelentés
BEFORE_COMMIT commit előtt
AFTER_COMMIT sikeres commit után
AFTER_ROLLBACK rollback után
AFTER_COMPLETION mindenképp a tranzakció végén

2.4 Domain event DDD-ben

DDD-ben a domain event azt mondja ki, hogy „valami fontos megtörtént a domainben”. Például OrderPlaced, PaymentCaptured, CustomerRegistered. Ezek jók lehetnek in-process eventként, majd később akár external eventként is publikálhatók, de a kettőt nem érdemes összemosni.

2.5 Event sourcing kapcsolat

Az event sourcing azt mondja, hogy az állapotot események sorozatából vezetjük le. Ez sokkal több, mint Spring application event használat. A két fogalom rokon, de nem azonos.

2.6 Internal vs external event

  • Internal event: JVM-en belül, gyors, egyszerű, de nem tartós.
  • External event: brokeren át megy, tartósabb, integrációra alkalmas.

3. Gyakorlati használat / Practical Usage

Jó példa, amikor egy user regisztráció után több mellékhatás kell: welcome email, audit log, default preferences, marketing consent sync. Ha ezeket mind a registerUser() metódusban hívod sorban, a use case hamar kövér lesz. Ehelyett publikálhatsz egy UserRegisteredEvent-et.

Másik tipikus hely a domain event egy aggregate művelet után. Például rendelés létrehozása után belső event jelezheti, hogy inventory reservation, loyalty pontszámítás vagy read model frissítés indulhat.

@TransactionalEventListener(AFTER_COMMIT) különösen fontos, ha csak akkor akarsz emailt küldeni vagy külső integrációt indítani, ha a DB commit tényleg megtörtént. Ellenkező esetben olyan side effect történhet, amihez nem tartozik valós állapotváltozás.

Ha az esemény feldolgozása drága vagy lassú, belső event helyett sokszor jobb külső message brokerre tenni. Az in-process event nem tartós, nincs natív replay, és node restart esetén elveszik.

Moduláris monolitokban különösen jól működik ez a minta. Az egyik modul közzétesz egy eseményt, a többi modul pedig saját felelősségben reagál rá. Így a kód nem csúszik át egy nagy service-labdába, ahol mindenki mindenkit közvetlenül hívogat. Ettől még az architektúra nem lesz automatikusan tökéletes, de a dependency-k sokkal kezelhetőbbek maradnak.

4. Kód példák / Code Examples

4.1 Egyszerű event publikálás

public record UserRegisteredEvent(UUID userId, String email) {}

@Service
class RegistrationService {
    private final UserRepository userRepository;
    private final ApplicationEventPublisher eventPublisher;

    RegistrationService(UserRepository userRepository,
                        ApplicationEventPublisher eventPublisher) {
        this.userRepository = userRepository;
        this.eventPublisher = eventPublisher;
    }

    @Transactional
    public UUID register(RegisterUserCommand command) {
        User user = userRepository.save(new User(command.email()));
        eventPublisher.publishEvent(new UserRegisteredEvent(user.getId(), user.getEmail()));
        return user.getId();
    }
}

4.2 Listener és transactional listener

@Component
class RegistrationListeners {

    @EventListener
    public void createDefaultPreferences(UserRegisteredEvent event) {
        System.out.println("Create defaults for " + event.userId());
    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void sendWelcomeEmail(UserRegisteredEvent event) {
        System.out.println("Send welcome email to " + event.email());
    }
}

4.3 Aszinkron event feldolgozás

@Configuration
@EnableAsync
class AsyncConfig {

    @Bean
    Executor applicationTaskExecutor() {
        return Executors.newFixedThreadPool(4);
    }
}

@Component
class AnalyticsListener {

    @Async
    @EventListener
    public void onUserRegistered(UserRegisteredEvent event) {
        System.out.println("Send analytics event for " + event.userId());
    }
}

4.4 Domain event aggregate-ből

class Order {
    private final List<Object> domainEvents = new ArrayList<>();

    void place() {
        // state transition
        domainEvents.add(new OrderPlacedEvent(id));
    }

    List<Object> domainEvents() {
        return List.copyOf(domainEvents);
    }
}

5. Trade-offok / Trade-offs

Előnyök:

  • lazább csatolás alkalmazáson belül;
  • tisztább use case metódusok;
  • utólag könnyebben bővíthető mellékhatások;
  • jó belépő event-driven gondolkodáshoz.

Hátrányok:

  • implicit vezérlés, nehezebb követni mi mire reagál;
  • synchronous listener blokkolhat vagy hibát dobhat vissza;
  • async listenernél bonyolódik a hibakezelés;
  • nem tartós, nem cross-service kommunikációra való alapból.

Mikor használd?

  • ha ugyanazon alkalmazáson belül akarsz decouplingot;
  • ha domain eventek mentén gondolkodsz;
  • ha egy tranzakció után több külön mellékhatás indul.

Mikor ne?

  • ha garantált tartósság kell;
  • ha más service-ek is fogyasztanák az eventet;
  • ha a csapat nehezen követi az implicit event láncokat.

6. Gyakori hibák / Common Mistakes

  1. Internal eventet external integrationként kezelni. Spring application event nem message broker.
  2. Nem érteni az alapértelmezett synchronoust. Emiatt váratlan latency és rollback jön.
  3. AFTER_COMMIT helyett sima @EventListener külső side effecthez. Ebből inkonzisztencia lesz rollbacknél.
  4. Túl sok technikai event. Minden setter változásból eventet gyártani zaj.
  5. Nincs error handling async listenerhez. Exception könnyen elveszik a háttérben.
  6. Ciklikus események. Egy listener újabb eventet publikál, ami visszacsatol és elszabadul.

7. Senior szintű meglátások / Senior-level Insights

A legfontosabb határvonal: a domain event fogalma nem egyenlő azzal, hogyan szállítod. Lehet, hogy a domainben megszületik egy OrderPlaced, de applicationen belül ezt először in-process hallgatók kezelik, majd egy külön komponens publikálja Kafkára. Ez egészséges szétválasztás.

A második fontos felismerés, hogy az implicit flow ára a felfedezhetőség. Ahogy nő a rendszer, az eseményekre reagáló helyek száma megszalad. Ilyenkor naming convention, dokumentáció és architekturális fegyelem kell, különben az események „láthatatlan vezérlési ugrások” lesznek.

A @TransactionalEventListener(AFTER_COMMIT) sokszor a minimum production higiénia. Ha az eseményből külső mellékhatás indul, a tranzakciós határt tudatosan kell kezelni. Ha pedig a feldolgozás kritikus és tartós kell legyen, jellemzően outbox + external broker az erősebb minta.

Event sourcinggal csak óvatosan. Az, hogy application eventeket használsz, még nem jelenti, hogy a rendszered event-sourced. Event sourcing teljes modell, saját olvasási, visszajátszási és verziózási következményekkel.

Senior nézőpontból az események namingje is stratégiai kérdés. A jó event múlt idejű, üzleti nyelvű és konkrét: InvoiceIssued jobb, mint InvoiceUpdatedSomehow. Ha a név homályos, a listener oldalon is homályos lesz a felelősség. Az esemény legyen szerződés, ne csak implementációs melléktermék.

8. Szószedet / Glossary

  • ApplicationEventPublisher: Spring komponens esemény publikálására.
  • @EventListener: annotáció eseménykezelő metódushoz.
  • @TransactionalEventListener: tranzakciós fázishoz kötött listener.
  • Domain event: üzletileg jelentős történést jelző esemény.
  • In-process: ugyanazon alkalmazáson belüli futás.
  • Async listener: külön threadben futó eseménykezelő.
  • Event sourcing: állapot eseményekből történő levezetése.
  • Outbox: megbízható external event publikálási minta.

9. Gyorsreferencia / Cheatsheet

Téma Rövid szabály Tipp
@EventListener alapból synchronous ne blokkolj sokáig
@Async listener leválasztja a hívót executor és error handling kell
@TransactionalEventListener tranzakcióhoz kötött external side effecthez gyakran AFTER_COMMIT
Internal event gyors és egyszerű nem tartós
External event brokeren át megy integrációra jobb
Domain event üzleti történést ír le ne technikai zaj legyen
Event sourcing külön architektúra ne keverd össze application eventtel

🎮 Játékok

8 kérdés