# Postmortem: Unexpected Transaction Nesting Behavior in TypeORM
## Summary
During database operation implementation, nested transaction blocks (`transaction` inside `transaction`) were implemented assuming independent transactions would be created. Instead, TypeORM reused the outer transaction context, causing unintended state propagation and rollback behavior.
## Root Cause
Transaction nesting behavior in TypeORM exhibits a non-standard pattern:
- Nested transactions implement a **savepoint pattern** rather than creating new autonomous transactions
- Inner transactions operate within the same database connection as the outermost transaction
- Transaction contexts inherit leak-prone state (e.g., query runners, isolation levels) from parent scopes
## Why This Happens in Real Systems
- ORM abstraction hides database differences (PostgreSQL vs MySQL vs SQL Server)
- Legacy API designs transported across ORM versions produce inconsistent behavior
- Documentation blurring between "transaction" vs "savepoint" terminology
- Asynchronous control flow obscures context propagation chains
## Real-World Impact
- Partial rollbacks triggering unexpected full-transaction rollbacks
- Undetectable commit skew due to shared isolation levels
- Deadlock risks when nested scopes acquire conflicting locks
- Client-side state pollution via context-contaminated entity managers
## Example or Code
```typescript
await dataSource.transaction(async (tx1) => {
await tx1.save(User, { id: 1, name: "Alice" });
await tx1.transaction(async (tx2) => {
// IN REALITY: Uses tx1's transaction context
// Only creates a SAVEPOINT if supported by DB
await tx2.save(Profile, { userId: 1, bio: "..." });
// This rollback only rolls back to savepoint
throw new Error("Inner failure");
});
// Outer transaction remains active but logically inconsistent
});
// Outer COMMIT executes despite inner rollback
How Senior Engineers Fix It
Core corrective strategies:
- Refactor to flat transactions: Initialize parallel transactions via distinct DataSource instances
- Explicit savepoint control:
await tx1.queryRunner.createSavepoint() try { /* inner work */ } catch { await tx1.queryRunner.rollbackToSavepoint() } - Boundary hardening:
- Inject entity managers instead of transaction contexts
- Verify database driver capabilities during startup
- Monitor transaction depth: Fail operations exceeding
maxNestedTransactionsthreshold - Compensating patterns: Implement saga transactions for distributed rollbacks
Why Juniors Miss It
Common oversight vectors:
- Mistaking TypeORM API unification for database-engine consistency
- Equating syntactic nesting with transactional isolation boundaries
- Lack of visibility into underlying driver mechanisms (QueryRunner internals)
- Insufficient testing of rollback edge cases across database types
- Over-reliance on simplified development databases (e.g., SQLite in-memory behavior)