In real application code these four letters get exercised together, in the same method, not tested one at a time — booking a seat is the natural example precisely because it genuinely needs all four:
public async Task<Booking> BookSeatAsync(int holdId, string paymentToken)
{
using var transaction = await _dbContext.Database.BeginTransactionAsync(IsolationLevel.ReadCommitted);
try
{
var hold = await _dbContext.Holds
.Where(h => h.Id == holdId)
.FirstOrDefaultAsync()
?? throw new InvalidOperationException("Hold not found — it may already have expired.");
if (hold.ExpiresAt < DateTime.UtcNow)
throw new InvalidOperationException("Hold expired."); // Consistency: enforce a business rule
var charge = await _paymentGateway.ChargeAsync(paymentToken, hold.PriceCents);
var booking = new Booking
{
SeatId = hold.SeatId,
CustomerId = hold.CustomerId,
PricePaidCents = charge.AmountCents,
BookedAt = DateTime.UtcNow,
};
_dbContext.Bookings.Add(booking); // Atomicity: part of one unit with the next two lines
_dbContext.Holds.Remove(hold);
_dbContext.Seats.Single(s => s.Id == hold.SeatId).Status = "sold";
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync(); // Durability: guaranteed to survive from this instant on
// Isolation (Read Committed here) meant no other transaction ever
// saw this seat sitting in a half-converted state between "held"
// and "sold" while this method was still in flight.
return booking;
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
Look at what BookSeatAsync is actually responsible for, and what it isn't. None of the four guarantees get built by hand anywhere in that method — the database supplies every one of them for free. What the code contributes is narrower: open one transaction around the related writes, choose a sensible isolation level, and check the business rule about an unexpired hold before letting the commit through. Almost every real ACID bug traces back to that narrower list being incomplete, not to a wrong idea about what a transaction does once it's open.