My Event-Driven Evolution: When Domain Events Weren’t Enough

Rekha PooniaRekha Poonia
3 min read

Clean architecture is great... until you're stuck debugging a "clean" mess at 2AM.
Here’s how I went from blindly using domain events for everything to building a more intentional, scalable, and realistic backend using manual integration event publishing, RabbitMQ, and SMTP.

Who Should Read This?

If you’ve ever:

  • Wondered why your event flow is suddenly spaghetti

  • Tried to “clean up” your code but ended up with 15 event handlers and no idea what triggered what

  • Wanted to decouple your app but also needed real-world control (hello, retry logic!)

…then you're gonna feel seen in this post. Especially if you're trying out Clean Architecture + .NET + RabbitMQ and dealing with things like sending emails after DB ops.

The Problem I Faced

At first, I did what most tutorials say — send emails directly from the handler:

Why this hurt:

  • If the email fails? Your DB operation fails too.

  • No retries unless you retry everything.

  • It blurred the line between core logic and side-effects.

  • You’re mixing responsibilities that should never sit next to each other.

So yeah — it looked "simple" but it was tech debt from day one.

First Attempt: Go Full Domain Events

Naturally, I said, “Let’s make it clean.”
So I refactored everything using domain events like a good Clean Architecture disciple:

This felt good. I was collecting domain events, dispatching them after save, and letting handlers do their thing — including sending emails.

But… then reality kicked in.

The Moment Domain Events Started to Suck

I started noticing weird things:

  • Email failures weren’t isolated — they could mess with other logic.

  • I couldn’t retry an email without replaying a domain event.

  • Debugging was a mess: “Where did this email even get triggered from?”

  • It became too "magic" — which sounds cool until you're the one maintaining it.

And here’s the biggest deal-breaker:
💬 Domain events should stay inside the domain.
Email isn’t part of my auction domain — it’s an external concern.


The Pivot: Manual Integration Event Publishing

At that point, I stopped trying to force everything into domain events.
Instead, I used them for internal behavior (like validation or audit logging) and manually published integration events for side-effects after SaveChanges().

That gave me:

  • Control over when an event gets published

  • Clear separation of domain logic vs external concerns

  • Reliable retry mechanisms without messing up core workflows


My Final Architecture (The Hybrid Approach)

  • Domain Events → For internal stuff (rules, history, etc.)

  • Manual Publishing to RabbitMQ → For external stuff (email, notifications)

Domain Event (Internal Workflow)

Manual Event Publish (After Save)

RabbitMQ Consumer (Email Worker)

Sending Email via SMTP (MailKit)

Tech Stack

  • .NET 8 + CQRS

  • RabbitMQ (message broker)

  • MailKit (SMTP Email)

  • Docker (for the background EmailWorker)

  • Clean Architecture + Custom EventBus


What I Learned

  • Domain events are ✨ amazing… for domain stuff. Don’t push them beyond that.

  • Manual publishing gives you explicit control — you know what, when, why.

  • Avoid coupling email to core domain logic at all costs.

  • Keeping email handling async + out-of-process makes your app waay more robust.

  • You don’t need to start with crazy infra — RabbitMQ + MailKit is simple and solid.


Final Thoughts

Clean architecture isn’t about purity — it’s about clarity.

And sometimes, clarity means knowing when not to follow the rulebook word for word. This hybrid approach gave me control, maintainability, and peace of mind. 🧘‍♀️


💬 Let’s Talk

Ever wrestled with the “Domain Events vs. Integration Events” dilemma?
Did you go full purist, or did you also end up drawing that line like I did?

Drop a comment or DM me — I’d love to hear your event journey.

0
Subscribe to my newsletter

Read articles from Rekha Poonia directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Rekha Poonia
Rekha Poonia

👩‍💻 Backend Developer | .NET Core | Clean Architecture & Microservices Enthusiast 📚 MCA in Cybersecurity | Passionate about building scalable, maintainable systems ✍️ Learning in public through technical blogs and side projects Currently focused on real-world backend projects, system design, and sharing insights with the developer community.