RFC-0005: After Naming Hook
- Status: Implemented
- Author: @revant.one
- Created: 2026-02-13
- Updated: 2026-02-21
- TSC Decision: Accepted
Summary
This RFC proposes adding a specific lifecycle hook after_naming (or on_name_assigned) to the BaseController. This hook is triggered after the document has received its final, human-readable name, but before the transaction is fully committed (or immediately after, depending on the implementation flexibility). This is crucial for the "Optimistic Naming" strategy where the document is first inserted with a UUID and then named in a subsequent step.
Motivation
In Phase 08, we adopted an Optimistic Naming Strategy to avoid row-level locking on a central Series table.
- Insert: Document inserted with UUID
id(Primary Key). - Naming Service: Calculates the next name (e.g., "INV-2024-001").
- Update: Updates the document row with
name="INV-2024-001".
The Problem:
Standard hooks like after_insert run before step 3 in this flow (or immediately after step 1).
- If a developer puts code in
after_insertto send an email ("Order Created"), the email might say "Order #None Created" or "Order #[UUID] Created". - We need a hook that guarantees
doc.nameis set and final.
Why after_naming and not reuse after_save?
after_saveruns on every update. Naming happens only once.- We want a specific event for "Identity Assigned".
Detailed Design
1. The Hook Definition
Added to BaseController:
class BaseController(Generic[T]):
async def after_naming(self, context: Any = None) -> None:
"""
Called after the human-readable 'name' has been generated and assigned
to the document.
Guarantees that self.doc.name is set.
"""
pass
2. Execution Flow in GenericRepository
async def save(self, entity):
# ... existing insert logic ...
# 1. Insert with UUID
await session.execute(insert_stmt)
# ...
# 2. If Naming Strategy is enabled
if self.has_naming_strategy:
new_name = await self.naming_service.generate_name(entity)
entity.name = new_name
await self.update_name(entity.id, new_name)
# 3. Trigger Hook
if controller:
await controller.after_naming()
3. Usage Example
class InvoiceController(BaseController[Invoice]):
async def after_naming(self):
# Safe to use self.doc.name here
await self.email_service.send(
subject=f"Invoice {self.doc.name} Created",
body="..."
)
Drawbacks
- Another Hook: Increases the surface area of the API.
- Timing Complexity: Developers might be confused whether to use
after_insertorafter_naming. (Guidance:after_insertfor DB ID existence,after_namingfor Human ID existence).
Alternatives
1. after_name (Property Setter)
- Frappe uses
autonameas a hook before saving to set the name. - Since we are doing Optimistic (post-insert) naming,
autonameimplies "calculate it now", whereasafter_namingimplies "it has been calculated and saved". after_namesounds like a property setter callback.after_namingsounds like a lifecycle event.
2. Use after_save with a check
async def after_save(self):
if self.doc.name and not self._previous_doc.name:
# logic
- Cons: Verbose and error-prone. Requires access to previous state which might not be easily available in all contexts.
Unresolved Questions
- Transaction Context: Should
after_namingrun in the same transaction as the name update? (Yes, generally, so if the hook fails, the name assignment rolls back).
Implementation Plan
- Add
after_namingtoBaseControllerprotocol. - Update
GenericRepositoryto call this hook after the naming service returns. - Document the lifecycle order clearly:
before_insert->insert->after_insert->naming->after_naming->commit.