Skip to main content

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.

  1. Insert: Document inserted with UUID id (Primary Key).
  2. Naming Service: Calculates the next name (e.g., "INV-2024-001").
  3. 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_insert to send an email ("Order Created"), the email might say "Order #None Created" or "Order #[UUID] Created".
  • We need a hook that guarantees doc.name is set and final.

Why after_naming and not reuse after_save?

  • after_save runs 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_insert or after_naming. (Guidance: after_insert for DB ID existence, after_naming for Human ID existence).

Alternatives

1. after_name (Property Setter)

  • Frappe uses autoname as a hook before saving to set the name.
  • Since we are doing Optimistic (post-insert) naming, autoname implies "calculate it now", whereas after_naming implies "it has been calculated and saved".
  • after_name sounds like a property setter callback. after_naming sounds 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_naming run in the same transaction as the name update? (Yes, generally, so if the hook fails, the name assignment rolls back).

Implementation Plan

  • Add after_naming to BaseController protocol.
  • Update GenericRepository to call this hook after the naming service returns.
  • Document the lifecycle order clearly: before_insert -> insert -> after_insert -> naming -> after_naming -> commit.