Notification Transactions and Save Boundaries
Summary
There is no explicit transaction anywhere in the Notification Service. No BeginTransaction, no TransactionScope and no execution strategy is used. Each SaveChangesAsync is its own implicit transaction, and a single event produces many of them.
Audience
Engineers, architects, QA, support and DevOps engineers.
Transaction boundaries
Each box is a separate implicit transaction. Nothing spans them.
Save inventory
Fifteen save sites exist in the reviewed service: nine in the dispatcher, five in the API handlers and one in the seeder.
| Site | What it commits |
|---|---|
| Audit helper | One audit row — called at every pipeline stage |
| Recipient upsert | One inserted or merged recipient |
| Message insert | One message row |
| Delivery success | Attempt row plus message state transition |
| Delivery retry | Attempt row plus counter and error |
| Delivery exhausted | Attempt row plus failed status |
| Sender missing | Failed status and error |
| Mark processed | One idempotency ledger row |
| Retry reset | Pending status and cleared error |
| API read-state | Status, timestamp and audit row together |
| API template create, update, test | Template or message and attempt rows |
Atomicity consequences
Event processing is not atomic. A crash partway through leaves committed messages, committed attempt rows and a committed partial audit trail, but no idempotency ledger row — because the ledger is written last. On redelivery the duplicate check finds nothing and the event is processed again from the beginning, producing duplicate messages and duplicate deliveries for the portion already completed.
This is the most consequential persistence characteristic in the module.
Delivery and state are co-committed but not with the send. The attempt row and the message state transition are added to the tracker and saved together, so they cannot diverge from each other. The provider send, however, has already happened before that save, so a failure to persist after a successful send loses the record of a notification that really was delivered.
Audit rows are independent. Each is its own save, so the audit trail can outlive a failed operation, recording a stage whose effect was rolled back or never committed.
The one multi-entity save
The read-state endpoint is the only site that commits two different entity types in one save: the message status update and its audit row. Every other site commits one entity type, or a message plus its attempt row.
Failure handling
No save is wrapped in a try-catch except inside the delivery loop, whose catch is scoped to the provider send rather than to persistence. A save failure — a constraint violation, a connectivity loss — therefore propagates as an unhandled exception. In the consumer path that causes a negative acknowledgement with requeue; in the API path it surfaces without a defined error contract.
Verified absence
No explicit transaction, no savepoint, no retrying execution strategy, no outbox pattern and no compensating action exists anywhere in the reviewed source.
Classification
Partial.
Requires confirmation
Whether the ledger-written-last ordering is intentional, and whether transactional event processing is planned, require confirmation.
Related Articles
See Also
Keywords
- Notification database
- Transactions
- Draft database documentation
Source References
microservices/src/notification-service/Application/NotificationDispatcher.csmicroservices/src/notification-service/Api/NotificationEndpoints.csmicroservices/src/notification-service/Infrastructure/NotificationDbContext.csmicroservices/src/notification-service/Seed/DefaultTemplateSeeder.cs
Revision Information
- Status: Draft
- Last reviewed: 2026-07-21
- Review cycle: Quarterly