What We Learned
When hooking embedding generation into the requirement create/update flow, the placement matters more than the implementation. We chose to call embedding generation after the successfulrepository.save(), wrapped in a blanket try/except that logs a warning and continues. The requirement is persisted before the AI call happens. If OpenAI is down, slow, or returns an error, the user’s data is already safe.
The alternative — generating the embedding first and including it in the save — means an OpenAI outage blocks requirement creation entirely. That’s a coupling failure: a search optimisation feature would degrade the core CRUD workflow that existed before AI features were added.
Why It Matters
AI API availability is fundamentally different from database availability. A database being down is a hard stop — you can’t save data without it. An embedding API being down is a quality degradation — you can’t semantically search requirements, but you can still create, read, update, and delete them. Treating these as equivalent by coupling them in the write path conflates a hard dependency with a soft enhancement. The fire-and-forget pattern also means the file-based storage backend (used in development and testing) works unchanged. The embedding hook checksDatabase.is_available() first. If there’s no Postgres connection, it returns silently. Zero disruption to the existing workflow.
The Pattern
except catches everything. This is one of the rare cases where a blanket catch is correct. The embedding call touches network I/O, JSON parsing, and database writes — any of which can fail in ways we haven’t anticipated. The consequence of missing an embedding is that one requirement won’t appear in semantic search results until the next update. The consequence of raising is that the user’s requirement creation fails for a reason unrelated to their action.