2.9 KiB
2.9 KiB
Unit of Work — Refactor Notes
Current Issues
1. Lifetime
UnitOfWork is created once in the handler constructor and reused across every Handle call.
It should be per-request, created inside Handle.
2. Transaction leaks out
Begin() returns *sql.Tx, which is then passed manually to each repo constructor.
The UoW should own that wiring — callers should never touch the transaction directly.
3. Eager instantiation (rejected solution)
Having UoW create all repos in Begin() solves the leaking transaction, but forces every handler
to pay for repos it doesn't need. Not acceptable.
Solution: Lazy Initialization
UoW exposes accessor methods that initialize each repo on first access, wired to the internal transaction. Unused repos are never instantiated.
type UnitOfWork struct {
FileStore *store.FileStore
tx *sql.Tx
tags ITagRepository
notes INoteRepository
}
func NewUnitOfWork() *UnitOfWork {
return &UnitOfWork{FileStore: store.NewFileStore()}
}
func (u *UnitOfWork) Begin() error {
tx, err := persistence.GetDBContext().DB.Begin()
if err != nil {
return err
}
u.tx = tx
return nil
}
func (u *UnitOfWork) Tags() ITagRepository {
if u.tags == nil {
u.tags = NewTagRepository(u.tx)
}
return u.tags
}
func (u *UnitOfWork) Notes() INoteRepository {
if u.notes == nil {
u.notes = NewNoteRepository(u.tx)
}
return u.notes
}
func (u *UnitOfWork) Commit() error {
if err := u.FileStore.Flush(); err != nil {
u.tx.Rollback()
u.tx = nil
return err
}
if err := u.tx.Commit(); err != nil {
u.FileStore.Rollback()
u.tx = nil
return err
}
u.tx = nil
return nil
}
func (u *UnitOfWork) Rollback() {
if u.tx == nil {
return
}
u.tx.Rollback()
u.FileStore.Rollback()
u.tx = nil
}
Usage in a Handler
func (h *CreateNoteHandler) Handle(ctx context.Context, raw json.RawMessage) (any, error) {
var cmd createNoteCommand
if err := json.Unmarshal(raw, &cmd); err != nil {
return nil, err
}
uow := repositories.NewUnitOfWork()
if err := uow.Begin(); err != nil {
return nil, err
}
defer uow.Rollback()
// Notes() is never called — never instantiated
dbTags, err := uow.Tags().GetByNames(ctx, tags)
if err != nil {
return nil, err
}
// ...
uow.FileStore.Stage(cmd.Path, data)
return cmd.Path, uow.Commit()
}
Properties of This Design
| Property | Result |
|---|---|
| Transaction leaks out of UoW | No — tx is private |
| Repos instantiated when not needed | No — lazy on first access |
| Handler controls which repos it uses | Yes — only accessed repos are created |
| UoW created per-request | Yes — inside Handle, not constructor |
| Handler has no persistent state | Yes — can be zero-value struct |