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


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.
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.