Architecture Proposal: Critical Path Stim-XP — User Coupling Decision
Status: Decided 2026-08-09: Option B as target, transitional exception recorded per Option C — migrated per OBJ-2370 Author: Sage (monthly review, 2026-07-03) Scope: Architectural decision (Phase A: documentation, shipped; Phase B: decision, decided; Phase C: migration, shipped as OBJ-2370)
Decision record: Josh decided Option C — Option B (route stim_* writes through an event + GamificationService) is the target state; Option A (a second sanctioned shared-kernel exception) was recorded here as a transitional record, not the decision itself, until the Option B migration shipped. The migration was tracked as its own issue, OBJ-2370 (own milestone, ~1 sprint, later downsized to a single phase per Orion's scoping) — see docs/architecture/ddd/03-context-map.md's Shared Kernel exception table and its "Resolved: Critical Path → Gamification (event-driven)" section for the resolved documentation.
Problem
CriticalPath::AwardStimXp writes directly to four stim_* fields on User — bypassing GamificationService and the shared-kernel rule documented in 03-context-map.md. Critical Path appeared in zero files under docs/architecture/ddd/ before this proposal, making it an undiscovered second write path to User progression data for any engineer or agent working from the DDD docs as their context map.
Evidence
| Claim | Source |
|---|---|
Shared-kernel rule: other contexts may read but not write to User directly; only Gamification (GamificationService.award_xp) is a documented exception | docs/architecture/ddd/03-context-map.md:84-86 |
| Phase 2 coupling-point inventory originally identified 5 tight coupling points | docs/architecture/ddd/06-modularization-guide.md:226-238 |
CriticalPath::AwardStimXp — direct writes to user.stim_xp_total, stim_streak_current, stim_streak_longest, stim_last_played_on via user.increment! and user.update!; no GamificationService call and no event publish | rails_api/app/interactions/critical_path/award_stim_xp.rb:21-27 |
CriticalPathPlay and StimXpLedger were absent from all docs/architecture/ddd/ files | docs/architecture/ddd/01-bounded-contexts.md, 03-context-map.md (now remediated by Phase A of this proposal) |
stim_xp_total, stim_streak_current, stim_streak_longest, stim_last_played_on confirmed on the users table | rails_api/db/schema.rb:1094-1097 |
Sustainability impact
- Dev velocity: Every future gamification-surface change must now be cross-checked against two independent write paths to
User. The Phase 2 coupling-point count (5, set before Critical Path shipped) under-covers the real coupling surface. - On-call burden: A
Userrow-lock contention or data-integrity incident touchingstim_xp_total/stim_streak_*previously had no DDD-doc guidance pointing an on-call engineer atCriticalPath::AwardStimXpas the writer. - Drift risk: A third gamification-adjacent feature shipping the same undocumented direct-User-write pattern makes eventual consolidation more expensive.
Options
Option A — Document as a second sanctioned shared-kernel exception
Add Critical Path to the DDD docs (01-bounded-contexts.md, 03-context-map.md, 06-modularization-guide.md) and mark CriticalPath::AwardStimXp as a second sanctioned exception alongside GamificationService.award_xp. No runtime changes.
Rationale for A: stim_xp_total is a distinct column from xp — the original Critical Path design may have intentionally kept a separate stim economy (a different score from goal XP). Option A preserves that separation with minimal risk.
Cost: Low (~1h doc work; Phase A below ships regardless of this decision). Risk: Normalises a two-writer pattern that may complicate future gamification consolidation.
Option B — Route stim writes through GamificationService (or equivalent)
Migrate stim_xp_total, stim_streak_current, stim_streak_longest, stim_last_played_on writes to route through GamificationService (or a new CriticalPathProgressionService), removing the direct User write from CriticalPath::AwardStimXp.
Rationale for B: Aligns with the Phase 2 event-driven direction and the "Gamification is the product" single-progression principle. One write path to all User progression fields.
Cost: Medium (~1 sprint; would become Phase C in its own milestone). Risk: Touches runtime behaviour; requires careful testing of the stim economy edge cases (idempotency guard, percentile ranking, streak calculation, theme unlock thresholds).
Migration path
Phase A — Documentation (~1h, ships regardless of A/B decision)
- Add Critical Path as a bounded context in
docs/architecture/ddd/01-bounded-contexts.md - Add Critical Path to the shared-kernel exception in
docs/architecture/ddd/03-context-map.md - Add
CriticalPath::AwardStimXpas a 6th entry in the Phase 2 coupling-point inventory indocs/architecture/ddd/06-modularization-guide.md - Ship this proposal stub
Phase A closes the discovery gap and stops further drift while Josh decides A vs B.
Phase B — Decision (requires human + Orion review)
Decided 2026-08-09. Josh picked Option C: Option B as the target state, Option A recorded as the transitional record until Option B ships (see the Decision record at the top of this document).
Phase C — Migration (OBJ-2370, shipped)
Shipped shape:
- Moved
CriticalPath::AwardStimXp's body verbatim toGamification::AwardStimXp, behaviour-identical CriticalPath::RecordPlaypublishescritical_path.stim_completed;Gamification::Subscriberssubscribes and callsGamification::AwardStimXpsynchronously- Direct
Userwrites removed fromrails_api/app/interactions/critical_path/entirely - DDD docs and context map updated to reflect the event-driven boundary (this issue, OBJ-2417)
Tracked as its own issue, OBJ-2370 — Orion downsized the original ~1 sprint milestone estimate to a single phase after scoping the actual diff (~150 lines, no schema change). Not part of the shared-kernel-write-guard PR (OBJ-1974/OBJ-2369). The row-lock-scoped idempotency guard and streak math in award_stim_xp.rb:12-27 carried over unchanged in the move; theme-unlock thresholds were confirmed to live read-side (fetch_stim_xp_status.rb, set_active_theme.rb), not inside that lock — see OBJ-2370 for the full write-up.
Risk if deferred
A third gamification-adjacent feature ships the same undocumented direct-User-write pattern, and the Phase 2 remediation scope keeps under-covering the real coupling surface. Each new instance makes eventual consolidation more expensive.
PBC check
- Free-tier infra capacity: Phase A and B are documentation and decision only — no infra impact. Phase C (if ever scoped) gets its own cost review at scoping time.
- Free-tier feature integrity: Phase A does not touch runtime behaviour. Phase C would require careful idempotency and stim-economy testing before shipping.
References
docs/architecture/ddd/01-bounded-contexts.md— bounded context registrydocs/architecture/ddd/03-context-map.md:84-86— shared-kernel exception ruledocs/architecture/ddd/06-modularization-guide.md:226-338— Phase 2 coupling-point inventoryrails_api/app/interactions/critical_path/award_stim_xp.rb— the coupling interactionrails_api/app/models/critical_path_play.rb— aggregate rootrails_api/app/models/stim_xp_ledger.rb— XP ledger entityrails_api/db/schema.rb:1094-1097— stim fields on the users table- Prior precedent:
docs/architecture/proposals/ddd-doc-refresh-may-2026.md(completed 2026-05-22, same drift pattern)
Last updated: 2026-08-10