# ADR-002: Write Mutex to Prevent SQLITE_BUSY Under Concurrent Load **Status:** Accepted **Date:** 2026-03-18 ## Context SQLite uses file-level locking. When multiple goroutines attempt write transactions concurrently, SQLite cannot acquire the write lock immediately and returns `SQLITE_BUSY`. Although the default Pragmas configure a 5-second busy timeout (`_timeout=5000`), this is a passive wait that still allows competing transactions to collide and fail under sustained concurrent write pressure. WAL mode (`_journal=WAL`) improves read concurrency but does not eliminate write contention: SQLite still allows only one writer at a time. ## Decision The `sqliteComponent` holds a `writeMu sync.Mutex` field. `NewUnitOfWork` detects when its `Client` argument is the concrete `*sqliteComponent` type and extracts a pointer to that mutex. `unitOfWork.Do` acquires the mutex before beginning a transaction and releases it after commit or rollback: ```go func (u *unitOfWork) Do(ctx context.Context, fn func(ctx context.Context) error) error { if u.writeMu != nil { u.writeMu.Lock() defer u.writeMu.Unlock() } tx, err := u.client.Begin(ctx) ... } ``` This serialises all write transactions at the application level, guaranteeing that only one writer reaches SQLite at a time and eliminating `SQLITE_BUSY` errors entirely. The mutex is only applied when using `NewUnitOfWork`. Callers who manage transactions manually via `Begin`/`Commit`/`Rollback` are not protected and must handle contention themselves. ## Consequences **Positive:** - `SQLITE_BUSY` is eliminated for all write workloads going through `UnitOfWork`. - Behaviour is deterministic and testable (see `TestUnitOfWork_WriteMutex`). - Reads are unaffected; the mutex only wraps writes. **Negative:** - Write throughput is bounded to one goroutine at a time. This is acceptable for SQLite's typical deployment profile (embedded, single-process, modest write rates). - The type assertion `client.(*sqliteComponent)` couples `NewUnitOfWork` to the concrete type. When a mock or alternative `Client` is supplied, `writeMu` is `nil` and serialisation is skipped silently. This is intentional for testing flexibility.