Skip to content

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

ClaimSource
Shared-kernel rule: other contexts may read but not write to User directly; only Gamification (GamificationService.award_xp) is a documented exceptiondocs/architecture/ddd/03-context-map.md:84-86
Phase 2 coupling-point inventory originally identified 5 tight coupling pointsdocs/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 publishrails_api/app/interactions/critical_path/award_stim_xp.rb:21-27
CriticalPathPlay and StimXpLedger were absent from all docs/architecture/ddd/ filesdocs/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 tablerails_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 User row-lock contention or data-integrity incident touching stim_xp_total/stim_streak_* previously had no DDD-doc guidance pointing an on-call engineer at CriticalPath::AwardStimXp as 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)

  1. Add Critical Path as a bounded context in docs/architecture/ddd/01-bounded-contexts.md
  2. Add Critical Path to the shared-kernel exception in docs/architecture/ddd/03-context-map.md
  3. Add CriticalPath::AwardStimXp as a 6th entry in the Phase 2 coupling-point inventory in docs/architecture/ddd/06-modularization-guide.md
  4. 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:

  1. Moved CriticalPath::AwardStimXp's body verbatim to Gamification::AwardStimXp, behaviour-identical
  2. CriticalPath::RecordPlay publishes critical_path.stim_completed; Gamification::Subscribers subscribes and calls Gamification::AwardStimXp synchronously
  3. Direct User writes removed from rails_api/app/interactions/critical_path/ entirely
  4. 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 registry
  • docs/architecture/ddd/03-context-map.md:84-86 — shared-kernel exception rule
  • docs/architecture/ddd/06-modularization-guide.md:226-338 — Phase 2 coupling-point inventory
  • rails_api/app/interactions/critical_path/award_stim_xp.rb — the coupling interaction
  • rails_api/app/models/critical_path_play.rb — aggregate root
  • rails_api/app/models/stim_xp_ledger.rb — XP ledger entity
  • rails_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

Loading…