A database transaction groups multiple statements so that either all of them commit together or none of them take effect, preserving consistency whenever a single feature performs more than one related write.
In simpler words
If a feature needs two or more writes to succeed together — like reassigning a ticket and logging who did it — a transaction makes sure a crash in the middle cannot leave only one of them done.
Repository/QueryBuilder calls outside a transaction each commit independently; TypeORM gives you an explicit way to group writes when that independence would be a bug.
Explain why an uncoordinated multi-write feature can leave inconsistent data
Use DataSource.transaction() with the transactional entity manager
Spot the common mistake of using the wrong repository inside a transaction
Why multi-write features need a transaction
Definition
Without a transaction, each save/update call commits to the database independently and immediately, so a failure between two related writes leaves the database in a state where one change happened and the other did not.
In simpler words
Reassigning a ticket to a new user while writing an audit-log row for that change is a good example: if the process crashes after the reassignment but before the log write, the audit trail becomes wrong forever.
Any feature described as “update this, and also record that” is a transaction candidate.
A transaction does not make writes faster; it makes them atomic — all-or-nothing — which is a correctness guarantee, not a performance one.
The failure a transaction prevents
// Without a transaction:
await ticketsRepo.update(ticketId, { assigneeId: newUserId }); // succeeds
// ...process crashes here...
await activityRepo.save({ ticketId, action: 'reassigned' }); // never runs
// Result: ticket reassigned, but no audit record — inconsistent
Both writes need to share one all-or-nothing outcome.
Using DataSource.transaction()
Definition
DataSource.transaction() runs a callback inside a single database transaction, passing it a transactional EntityManager that all reads and writes inside the callback must use so they participate in that same transaction.
In simpler words
The transaction commits automatically if the callback resolves, and rolls back automatically if it throws — you rarely write explicit COMMIT/ROLLBACK by hand.
Every repository used inside the callback must come from the passed-in manager (manager.getRepository(Entity)) — a repository injected the normal way runs outside the transaction.
Throwing inside the callback is the correct way to abort — TypeORM rolls back everything the callback did so far.
this.dataSource is injected with @InjectDataSource() from @nestjs/typeorm.
Where this goes wrong
Definition
A transaction only protects the writes performed through its transactional manager; using an injected, non-transactional repository or a separate query inside the same callback silently escapes the transaction.
In simpler words
The most common bug is calling the ordinary @InjectRepository(Ticket) instance inside a transaction() callback instead of manager.getRepository(Ticket) — that write commits immediately and independently, defeating the whole point.
Always resolve every repository used inside the callback from the manager argument, not from constructor injection.
Keep unrelated, independent writes outside the transaction — wrapping everything in one transaction just to be safe adds contention without adding correctness.
Mistake: mixing injected and transactional repos
// Wrong — this.ticketsRepo is NOT part of the transaction below
await this.dataSource.transaction(async (manager) => {
await this.ticketsRepo.update(ticketId, { assigneeId: newUserId }); // escapes!
await manager.getRepository(TicketActivity).save({ ... });
});
// Right — both repos come from the transactional manager
await this.dataSource.transaction(async (manager) => {
await manager.getRepository(Ticket).update(ticketId, { assigneeId: newUserId });
await manager.getRepository(TicketActivity).save({ ... });
});
this.ticketsRepo commits on its own the moment update() resolves, regardless of what the transaction later does.
Keep transactions short — no external awaits inside
Definition
A database transaction holds locks and a pooled connection for its duration; awaiting slow external work (HTTP calls, email, queues) inside that transaction extends lock time and exhausts the connection pool under load.
In simpler words
Do DB work atomically, commit, then talk to the outside world — or use an outbox/queue pattern when both must eventually succeed.
Bad: begin transaction → update ticket → await fetch(thirdParty) → write audit → commit. The third-party call holds a DB connection hostage.
Better: transaction for ticket + audit → commit → then publish to RabbitMQ / call the third party. Compensate if the external call fails.
Isolation intuition: default READ COMMITTED is enough for most Nest CRUD; reach for stronger isolation only when you can name the anomaly you are preventing.
Mistake: external I/O inside a transaction
// Wrong
await this.dataSource.transaction(async (manager) => {
await manager.getRepository(Ticket).save(ticket);
await fetch('https://billing.example/charge'); // holds DB connection!
await manager.getRepository(TicketActivity).save(activity);
});
// Right — DB atomicity first, side effects after
await this.dataSource.transaction(async (manager) => {
await manager.getRepository(Ticket).save(ticket);
await manager.getRepository(TicketActivity).save(activity);
});
await this.billingClient.charge(...); // outside
Transactions protect database consistency; they are not a general “undo the internet” mechanism.
Keep in mind
Ask “does this feature make more than one related write?” — if yes, it is a transaction candidate.
Every repository inside a transaction() callback must come from that callback’s manager.
Throw to roll back; return normally to commit — do not manage commit/rollback by hand unless using the lower-level QueryRunner API.
Test
Check your understanding
At least 10 questions — mix of concept, syntax, practical, and logic. Score ≥ 80% (enforced by the API) to save progress.