Escaping Abstraction Hell


The setup: Here I’m using the famous MediatR by Jimmy Bogard to send my commands using the IRequest interface, where I have a few customized pipeline behaviors. And on this blog i’m gonna show what I consider a clean approach on how to handle transactions. Other devs got an acronym for this ACID.
But I would just dive into the most important, the A for Atomicity.
"All or nothing."
A transaction must either complete fully or not at all. If any part of the transaction fails, all changes must be rolled back.
Developers use transaction scopes for this reason - and a usual code goes like this.
A very self-explanatory piece of code. Here, we are interacting with two repositories. A repository is the infamous repository pattern — just an abstraction layer over your DbContext
.
Suppose we're using integers as our IDs. In that case, we typically need to call SaveChanges
twice: once to generate the ID and again to insert that ID into the Product
. But we can avoid this by using strings as identifiers — or even better, ULIDs (Universally Unique Lexicographically Sortable Identifiers), which I consider the new standard in the .NET world.
📝 [My blog about ULIDs (Universally Unique Lexicographically Sortable Identifiers)]
❌What’s wrong with the current approach?
Calling
SaveChanges
twice — this can be avoided by using string-based identifiers like ULIDs instead of relying on database generated IDs.Try-catch and transaction scopes in every handler — most developers end up manually wiring transactions inside each command handler. Worse, some introduce a heavyweight Unit of Work layer that bloats the codebase. This results in an "abstraction hell" — where every
DbContext
has its own repository, and then you add yet another abstraction on top of that?
A more elegant solution, in my opinion, is to create a Transaction Behavior.
MediatR is an excellent tool that allows you to inject and chain behaviors in your request pipeline. With this, you can define a behavior that wraps your handlers in a transaction — executing code before and after the request — removing the need to clutter each handler with boilerplate transaction code.
What’s going on?
Here I inserted the transactions scopes and the rollback logic. Also I added a transaction attribute where I can decorate my commands that is being sent by IRequest.
The attribute is not needed but I just want to have more control on my codebase. This removes your problem also on your handlers where you will need to add try-catch blocks to catch your generic exceptions.
Again, software development is an abstract for me— we define our own standards, and we define what “clean” truly means in our context. This implementation reflects my preference:
But At the end of the day, this is about building systems that are not just correct, but sustainable and expressive for the team maintaining them.
Subscribe to my newsletter
Read articles from Juan Miguel Nieto directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Juan Miguel Nieto
Juan Miguel Nieto
A software developer trying to write organic blogs.