We have a homebrew transaction management. We might want to use the built-in .net transactions.
This is my take on this subject after a careful review.
What “transactions” mean in Origam
A transaction is a logical unit of work that needs to be all‑or‑nothing: either all operations succeed or they’re all undone. In Origam this can involve several kinds of resources: a database, files on disk, work‑queue messages, or emails. To make sure those operations stay in sync, Origam assigns a transaction identifier to each unit of work. Every service call (IServiceAgent) carries this TransactionId so that downstream components know which transaction they’re part of.
How Origam implements transactions today
Origam uses its own transaction manager called ResourceMonitor. When a transaction begins, each resource that needs to participate (database, file lock/delete, email fetch, etc.) registers an object derived from OrigamTransaction. These objects are stored in an ordered dictionary keyed by resource ID. The manager supports three basic operations:
- Commit. When we want to finalize the transaction, the manager loops through the registered
OrigamTransactionobjects in reverse order of registration and calls their Commit method. If any commit throws an exception, it triggers a rollback of the entire transaction.
There is no guarantee that the database commit runs first or last; it depends on the order in which resources were registered. - Rollback. On rollback, the manager loops through all registered transactions and calls each one’s Rollback method. It also cleans up the internal transaction store so that stale transactions don’t hang around.
- Save points. There is also support for save‑points, but those are outside the scope of this discussion.
Each resource implements its own commit and rollback. For example, FileDeleteTransaction holds a list of open file streams; when Commit runs, it closes each stream and permanently deletes the file. Its Rollback just closes the streams; it cannot restore a deleted file.
This illustrates the current limitation: if a file is deleted and then a later commit fails, Origam can’t bring that file back.
Why I thought about it
TransactionScope is a .NET API that hides transaction details: you wrap code in a using block and the framework manages commits and rollbacks for you. In theory it can also coordinate multiple resources using a two‑phase commit (2PC) protocol via Microsoft’s Distributed Transaction Coordinator (MSDTC). The original idea behind suggesting TransactionScope was:
- Less custom code. Maybe we could delete our home‑grown transaction manager and rely on a standard library.
- Automatic 2PC. Perhaps we would get atomic commits “for free” even across different resources.
- Transactional file system. Microsoft used to have an implementation of file system transactions and I thought we would benefit from it. Unfortunately it’s not the case anymore. It turns out we had file transactions both before and after Microsoft.
Why I am rethinking that
After exploring both the code and the .NET behaviour, several issues became clear:
- Ambient vs. explicit context.
TransactionScopeuses ambient transactions – a hidden context (Transaction.Current) that flows implicitly. This means that when you look at a method signature, you have no idea whether it is running in a transaction. With Origam’s explicitTransactionId, every caller and callee knows exactly which transaction they’re in. - Debugging. Because we use explicit identifiers, logs and diagnostics can always show which transaction a message belongs to. With
TransactionScope, the transaction ID is buried in the call context; correlating logs becomes much harder. - Non‑database resources. Many Origam operations (file locks, file deletes, emails) can’t enlist in a .NET transaction out of the box. To work with
TransactionScope, each resource would need to implement theIEnlistmentNotificationinterface and handlePrepare,Commit,Rollback, andInDoubtcallbacks. That is a non‑trivial refactor. - Two‑phase commit is not a silver bullet. True 2PC can coordinate multiple durable resources, but it requires a transaction coordinator (MSDTC) and changes the failure modes. Even with 2PC, if a participant makes an irreversible change during the prepare phase, a coordinator failure can leave resources inconsistent. Our file‑deletion example shows that simply calling
Delete()cannot be reversed; 2PC would force us to implement a “prepare” stage that only moves or locks files rather than deleting them outright. - Large code change, little benefit. Adopting
TransactionScopewould not be a drop-in change. It would require rewriting the transaction manager and updating every resource to enlist properly. For our use‑case (one primary database plus a few local resources) this complexity doesn’t buy us much. We would still need to deal with irreversible side‑effects manually.
A better plan: add a prepare/commit phase to Origam
Rather than replacing our transaction manager, we can extend it to mimic the most valuable part of 2PC in case the need comes:
- Add a Prepare method to
OrigamTransaction. In the prepare phase, each resource performs only reversible actions. For example, instead of immediately deleting a file, theFileDeleteTransactioncould move it to a “pending deletion” location. If any prepare fails, we immediately roll back and no irreversible actions have taken place. - Commit only after all resources prepare. Once all participants indicate they are ready, the manager calls their
Commitmethods. At this point, irreversible actions (e.g. deleting the file) can take place. As we do not have access to thePrepare()method of our database transaction, we would have to make sure weCommitthe database transaction first (as the database engine makes sure it preserves atomicity) and only then finally commit all our own resources (files, e-mails). See Commit method - Rollback on failure. If any commit fails, we call
Rollbackon all participants. Because the prepare phase avoided irreversible changes, rollback can always undo the prepare work. See Rollback method
This small change would make transaction boundaries safer without requiring us to adopt the complexity of distributed transactions. It keeps our transaction identifiers explicit, preserves our current debugging simplicity, and avoids hidden ambient context and huge code rewrites.
But as the only place where mail and file transactions are used are basically work queues where the transactional behavior is covered, we might not just change much or anything at all. If we would e.g. like to introduce transactions to outputting files, we would need much more robust 2PC, with a transaction log and recovery after instance crash.
Summary
In summary, Origam’s existing transaction system is simple, explicit and tuned to our needs. While the .NET TransactionScope API is attractive in theory, it’s not a good fit for our mixed environment of databases, files and email. We can achieve the safety we need by adding a prepare phase to our own transaction manager and ensuring that resources don’t do anything irreversible until they’re sure the overall transaction will commit. This gives us the practical benefits of two‑phase commit without the complexity and dependencies that come with TransactionScope.