.NET
Transactions
Programming
Software Development
Database Management

Transactions in .net

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

Transactions in .NET exist to keep a group of operations consistent: either all succeed or all are rolled back. The practical challenge is choosing the right transaction API for the level you are working at, such as raw ADO.NET, TransactionScope, or an ORM like EF Core. The best choice depends on how many resources are involved and how explicit you need the control to be.

What a Transaction Guarantees

A transaction gives you the classic ACID guarantees:

  • atomicity
  • consistency
  • isolation
  • durability

In application code, the two most visible guarantees are:

  • if one step fails, earlier writes are rolled back
  • concurrent work sees a controlled view of in-progress changes

Without transactions, partial updates are easy to create and hard to repair.

ADO.NET SqlTransaction

When you are using raw SQL commands against one database connection, SqlTransaction is the most direct tool.

csharp
1using System;
2using System.Data.SqlClient;
3
4string connectionString = "Server=.;Database=AppDb;Trusted_Connection=True;";
5
6using var connection = new SqlConnection(connectionString);
7connection.Open();
8
9using var transaction = connection.BeginTransaction();
10
11try
12{
13    var cmd1 = new SqlCommand(
14        "INSERT INTO Accounts(Name, Balance) VALUES (@name, @balance)",
15        connection,
16        transaction);
17    cmd1.Parameters.AddWithValue("@name", "Alice");
18    cmd1.Parameters.AddWithValue("@balance", 100);
19    cmd1.ExecuteNonQuery();
20
21    var cmd2 = new SqlCommand(
22        "UPDATE AuditLog SET Message = @msg WHERE Id = 1",
23        connection,
24        transaction);
25    cmd2.Parameters.AddWithValue("@msg", "Account inserted");
26    cmd2.ExecuteNonQuery();
27
28    transaction.Commit();
29}
30catch
31{
32    transaction.Rollback();
33    throw;
34}

This is explicit and predictable. It is a good default when the work is confined to one connection.

TransactionScope for Ambient Transactions

TransactionScope creates an ambient transaction that participating operations can join automatically.

csharp
1using System;
2using System.Transactions;
3
4using var scope = new TransactionScope(
5    TransactionScopeOption.Required,
6    new TransactionOptions
7    {
8        IsolationLevel = IsolationLevel.ReadCommitted
9    },
10    TransactionScopeAsyncFlowOption.Enabled);
11
12// database operations here
13
14scope.Complete();

If Complete() is not called, the transaction is rolled back when the scope is disposed.

This approach is convenient, but it can become harder to reason about if too much code participates implicitly.

EF Core Transaction Example

If you are using EF Core, you can either rely on SaveChanges() transaction behavior for simple units of work or open an explicit transaction for multi-step workflows.

csharp
1using var db = new AppDbContext();
2using var tx = await db.Database.BeginTransactionAsync();
3
4try
5{
6    db.Customers.Add(new Customer { Name = "Alice" });
7    await db.SaveChangesAsync();
8
9    db.AuditEntries.Add(new AuditEntry { Message = "Customer created" });
10    await db.SaveChangesAsync();
11
12    await tx.CommitAsync();
13}
14catch
15{
16    await tx.RollbackAsync();
17    throw;
18}

This is the right pattern when multiple SaveChanges calls must succeed or fail together.

Async Code and Transactions

Async flows add an important detail: not every transaction API works correctly unless async flow is enabled or the ORM supports async transaction boundaries directly.

For TransactionScope, use:

csharp
TransactionScopeAsyncFlowOption.Enabled

Without it, async code can break ambient transaction flow in confusing ways.

For explicit ADO.NET or EF Core transactions, prefer the async transaction methods when the surrounding code is async.

Choose the Right Isolation Level

Isolation level affects both correctness and concurrency.

Typical choices:

  • 'ReadCommitted: common default'
  • 'RepeatableRead: stronger consistency for repeated reads'
  • 'Serializable: strongest isolation, highest contention risk'

Do not increase isolation casually. Higher isolation can reduce concurrency and create lock contention under load.

Distributed Transactions Need Caution

If one logical unit of work touches multiple resource managers, such as multiple databases or a database plus a message queue, ambient transactions can become distributed transactions. Those add operational complexity and may not be supported everywhere.

In modern systems, many teams avoid this by:

  • using an outbox pattern
  • separating operations into local transactions
  • designing for eventual consistency where appropriate

Do not reach for distributed transactions unless the consistency requirement clearly justifies them.

Common Failure Pattern

A typical bug looks like this:

csharp
// first update succeeds
// second update throws
// no transaction means partial state remains

That is exactly the scenario transactions are meant to prevent. If the operations belong to one business action, they should usually live in one transaction boundary.

Common Pitfalls

One common mistake is making the transaction scope too large, such as wrapping slow network calls together with database writes. That increases lock time and reduces throughput.

Another mistake is forgetting to call Complete() on TransactionScope, which results in rollback even when the code appears to succeed.

Developers also mix multiple connections and ORMs without understanding whether they are joining the same transaction context.

Finally, using transactions for every read path can add unnecessary cost. Not every operation needs a custom transaction boundary.

Summary

  • Use transactions when several operations must succeed or fail as one unit.
  • Prefer SqlTransaction for explicit one-connection ADO.NET workflows.
  • Use TransactionScope carefully when ambient transaction flow is helpful.
  • In EF Core, use explicit database transactions for multi-step save workflows.
  • Keep transaction boundaries small and choose isolation deliberately.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.