İçeriğe geç

Domain Olayları — Hesap Açılmadı, Hoş Geldin E-postası Neden Gitti?

Orta 8 dk Sık karşılaşılır

Önce şunu oku: Zengin Domain Modeli — JPA Entity'de Kapsülleme

30 saniyede özet

Hesap açılınca e-posta ve puan gibi yan işler olur. Servisin hepsini çağırması onu herkese bağlar; olay kullanmak kodu ayırır ama hatayı ayırmaz. Geri alınamayan yan işler commit'ten sonra çalışan bir dinleyiciye gider.

Bir kulübün kayıt görevlisi her yeni üyede posta odasını ve hediye masasını tek tek arıyor. Bir gün kayıt son anda iptal oluyor, ama hediye çoktan yola çıkmış. Başka bir kulüpte görevli yalnızca panoya bir not asıyor: “yeni üye katıldı”.

  1. Bayt: Hesap açma servisi önce kaydı yazıyor, sonra e-posta ve puan servisini çağırıyor.

  2. Sen: Bugün puan servisi çöktü. Kimse hesap açamadı; ama herkese hoş geldin e-postası gitti!

  3. Bayt: Puan bir hediye. Hediye yok diye müşteri bankaya giremiyor mu?

  4. Bayt: Kayıt kesinleşince panoya bir not asalım; ilgilenen okusun.

Asıl iş herkesi tanımasın

Hesap servisi e-postayı, puanı ve risk kontrolünü kendisi çağırırsa, her yeni yan iş onu değiştirir. Servis, ilgisi olmayan servisleri tanımak zorunda kalır.

Bir domain olayıDomain'de olmuş bir şeyi geçmiş zamanla anlatan nesne: AccountOpened. Kimin dinleyeceğini bilmez.Sözlükte gör → bunu ayırır. aggregateBirlikte tutarlı kalması gereken ve tek bir kök nesne üzerinden değiştirilen nesne kümesi. Dışarıdan yalnızca köke erişilir; başka aggregate'lara id ile referans verilir.Sözlükte gör → “AccountOpened” olayını kaydeder; e-posta ve puan bu olayı dinler. Hesap servisi artık onların varlığını bilmez.

Kafam karıştı, daha basit anlat

Asıl iş yalnızca olanı söyler. Yan işler o olayı dinler.

Hızlı kontrolBaşlangıç

Domain olayı nedir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Hesap servisi e-posta, puan ve risk servisini doğrudan çağırıyor. Sorun nedir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Kodu ayırmak, hatayı ayırmak değil

Olayı @EventListener ile dinliyoruz; dinleyici aynı transaction içinde çalışıyor. Puan servisi hata verdi. Hesap açılır mı? Cevabı göster

Açılmaz. Dinleyici aynı transaction içinde çalıştığı için hata yukarı çıkar ve hesabın kaydı da geri alınır. Kod ayrıldı, ama hata ayrılmadı; e-posta da çoktan gitmişti.

Kayıt kesinleşince panoya as; ilgilenen okusun.
Adım adım oku
  1. Kayıt görevlisi herkesi tek tek arıyor.
  2. Kayıt iptal oldu, hediye çoktan gitti.
  3. Panoya not: yeni üye katıldı.
  4. Kayıt kesinleşince panoya as; ilgilenen okusun.

Spring’in @TransactionalEventListenerBir olayı transaction'ın belli bir aşamasında, varsayılan olarak commit'ten sonra işleyen Spring dinleyicisi.Sözlükte gör →’ı olayı commit’ten sonra işler. Commit başarısız olursa dinleyici hiç çalışmaz; açılmayan hesaba e-posta gitmez.

Dinleyicideki hata da artık asıl işi geri almaz. Hesap açık kalır, puan sonra yeniden denenir.

Kafam karıştı, daha basit anlat

Geri alınamayan yan işi commit’ten sonra yap. Yan işin hatası asıl işi bozmasın.

Hızlı kontrolOrta

@EventListener aynı transaction içinde çalışırken puan servisi hata verirse ne olur?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

@TransactionalEventListener(phase = AFTER_COMMIT) ne sağlar?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Kendin gör

Hesap açılınca e-posta ve puan

Tohum 822029

Hesap servisinin doğrudan tanıdığı yan servis: 2

    Hız
    Adım 0

    Şu an ne oldu?

    Servis e-postayı ve puanı kendisi çağırıyor

    Puan servisi çöktü

    Görevler0/3

    • Açılmayan hesaba hoş geldin e-postası gitsinaçık

      İpucu

      Commit başarısız olsun.

    • Puan servisi yüzünden hesap açılamasın, olayla bileaçık

      İpucu

      Aynı transaction içinde dinleyici.

    • Puan servisi çökse de hesap açılsınaçık

      İpucu

      Commit sonrası dinleyici.

    Olay günlüğü (0)

    Henüz olay yok. Oynat veya adımla.

    1. Varsayılanla oynat. Servis her şeyi kendisi çağırıyor, puan servisi çöktü: hesap açılmadı, e-posta yine de gitti.
    2. “@EventListener, aynı transaction” seç. Servis artık kimseyi tanımıyor; ama sonuç aynı.
    3. “Commit başarısız” seç. Hesap yok, e-posta ve puan gitti.
    4. “AFTER_COMMIT” seç ve iki hatayı da dene. Commit başarısızsa kimseye bir şey gitmez; puan servisi çökse de hesap açılır.
    Hızlı kontrolOrta

    E-posta commit'ten önce gönderildi, sonra commit başarısız oldu. Ne olur?

    Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

    Spring Data'da bir aggregate olayı nasıl kaydeder?

    Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

    Tuzaklar

    Kesin teslim beklemek. Commit sonrası dinleyici çalışırken uygulama çökerse olay kaybolabilir. Kaybolmaması gereken olaylar aynı transaction’da bir outboxOlayı, iş verisiyle aynı yerel transaction içinde bir tabloya yazıp ayrı bir süreçle kuyruğa taşıma deseni. İki kaynağa yazma (dual write) problemini çözer.Sözlükte gör → tablosuna da yazılır.

    Commit sonrası veritabanına yazmak. O anda asıl transaction bitmiştir. Dinleyici yazacaksa yeni bir transaction açmalıdır (REQUIRES_NEW).

    Her şeyi olaya taşımak. Asıl işle birlikte olması ya da hiç olmaması gereken bir iş (örneğin ledger satırı) olaya bırakılmaz; aynı transaction içinde yapılır.

    Kafam karıştı, daha basit anlat

    Kaybolmaması gerekeni outbox’a yaz, commit sonrası yazarken yeni transaction aç, ayrılmaz işi ayırma.

    Hızlı kontrolOrta

    AFTER_COMMIT dinleyicisi çalışırken uygulama çökerse olay ne olur?

    Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.
    Derinleş · Hesap açılışı: olay commit'ten sonra 5 dosya · ~81 satır · ilk okumada atlayabilirsin
    Proje dosyaları

    accounts/src/main/java/com/bank/accounts/ Account.java Aggregate: hesap açılınca AccountOpened olayını kaydeder; kimin dinleyeceğini bilmez.

    accounts/src/main/java/com/bank/accounts/Account.java
    @Entity
    @Table(name = "accounts")
    public class Account extends AbstractAggregateRoot<Account> {
    @Id
    private UUID id;
    @Column(name = "customer_id", nullable = false)
    private UUID customerId;
    @Column(nullable = false, unique = true)
    private String iban;
    protected Account() {
    }
    public static Account open(UUID customerId, String iban) {
    Account account = new Account();
    account.id = UUID.randomUUID();
    account.customerId = customerId;
    account.iban = iban;
    // A fact, in the past tense; the account does not know who listens.
    account.registerEvent(new AccountOpened(account.id, customerId, iban));
    return account;
    }
    public UUID id() {
    return id;
    }
    }

    accounts/src/main/java/com/bank/accounts/ AccountOpened.java Servis: yalnızca hesabı açar ve kaydeder; e-posta ve puandan habersiz.

    accounts/src/main/java/com/bank/accounts/AccountOpened.java
    public record AccountOpened(UUID accountId, UUID customerId, String iban) {
    }

    accounts/src/main/java/com/bank/accounts/ OpenAccountService.java E-posta dinleyicisi: commit'ten sonra çalışır; açılmayan hesaba e-posta gitmez.

    accounts/src/main/java/com/bank/accounts/OpenAccountService.java
    @Service
    class OpenAccountService {
    private final AccountRepository accounts;
    private final IbanGenerator ibans;
    OpenAccountService(AccountRepository accounts, IbanGenerator ibans) {
    this.accounts = accounts;
    this.ibans = ibans;
    }
    @Transactional
    public UUID open(UUID customerId) {
    Account account = Account.open(customerId, ibans.next());
    accounts.save(account); // publishes AccountOpened to the listeners
    return account.id();
    }
    }

    notifications/src/main/java/com/bank/notifications/ WelcomeMail.java Puan dinleyicisi: commit'ten sonra, kendi transaction'ında; hata hesabı etkilemez.

    notifications/src/main/java/com/bank/notifications/WelcomeMail.java
    @Component
    class WelcomeMail {
    private final Mailer mailer;
    WelcomeMail(Mailer mailer) {
    this.mailer = mailer;
    }
    // Runs only after the account row is committed; a rolled-back opening sends nothing.
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    void on(AccountOpened event) {
    mailer.sendWelcome(event.customerId(), event.iban());
    }
    }

    loyalty/src/main/java/com/bank/loyalty/ WelcomePoints.java

    loyalty/src/main/java/com/bank/loyalty/WelcomePoints.java
    @Component
    class WelcomePoints {
    private final PointsClient points;
    WelcomePoints(PointsClient points) {
    this.points = points;
    }
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    void on(AccountOpened event) {
    // A failure here is logged and retried; the account stays open.
    points.grantWelcome(event.customerId());
    }
    }

    Kendini sına

    Şimşek turu1/4

    Domain olayı, kimin dinleyeceğini bilmeden olanı anlatır.

    Soru 1/3İleri

    AFTER_COMMIT dinleyicisi veritabanına yazmak istiyor. Dikkat edilmesi gereken nedir?

    Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

    Aklında kalacak üç şey

    1. 1 Domain olayı olmuş bir şeyi geçmiş zamanla anlatır. Asıl iş, kimin ilgileneceğini bilmek zorunda kalmaz.
    2. 2 Aynı transaction içinde çalışan dinleyici kodu ayırır ama hatayı ayırmaz: yan işteki hata asıl işi de geri alır.
    3. 3 E-posta gibi geri alınamayan yan işler commit'ten sonra yapılır. Kesin teslim gerekiyorsa olay bir outbox tablosuna da yazılır.
    0/4 kart bu dersten toplandı