Application Services
Every Interaction::Base subclass in app/interactions/ is an application service — a use case that orchestrates domain logic. This document maps each interaction to its bounded context, classifies its DDD role, and flags cross-context coupling.
Classification Key
| Type | Description |
|---|---|
| Application Service | Orchestrates a use case. Coordinates aggregates, calls domain services, triggers side effects. |
| Domain Service | Pure business logic that doesn't belong to any single aggregate. Stateless. |
| Query Service | Read-only. Aggregates data from one or more contexts without side effects. |
Namespace Placement Rule
An interaction lives in the namespace of the bounded context that owns the aggregate it mutates — not the namespace of whichever context happens to share its verb. 01 — Bounded Contexts states the same principle for the two unnamespaced enneagram interactions ("should be UserIdentity::…"); this section makes it explicit and gives it a worked example.
Worked example — UserNotification: the model is unambiguously owned by Social & Community (rails_api/app/models/user_notification.rb:3 — # Context: Social & Community (Accountability)). That ownership splits into two different responsibilities:
- Producing a notification (
UserNotification.generate) is open to every context — it's shared delivery infrastructure (the model header calls it "Entity (delivery infrastructure)"). Gamification, Teams, Feedback, and AI all legitimately call it today. - Managing a notification's lifecycle or read-state — acknowledge, acknowledge-all, delete, list — belongs to the context that owns the aggregate: Social & Community.
AcknowledgeNotification and AcknowledgeAllNotifications lived under Gamification:: for a historical reason (the shared verb "Acknowledge," which AcknowledgeAction also uses for badges) until OBJ-2950 moved them into Social::, alongside the pre-existing DeleteNotification and BuildNotificationHistory. AcknowledgeAction itself stays in Gamification — it manages UserAction (badges), which Gamification owns, so the rule places it correctly as-is.
Interaction Registry
Goal Tracking
| Interaction | Type | Description | Side Effects | Coupling |
|---|---|---|---|---|
AddGoal | Application Service | Create a goal with type, category, recurrence, optional milestones | Creates initial GoalEvent, triggers badge jobs (CreateFirstGoalActionJob, CreateSecondCategoryActionJob, CreateSecondTypeActionJob) | Tight to Gamification — inline badge detection and job triggers. Should publish GoalCreated event. |
UpdateGoal | Application Service | Edit goal properties, mark complete/incomplete, soft delete | Creates completion event if newly completed | Self-contained |
AddGoalEvent | Application Service | Log a progress update on a goal | Creates GoalEvent, triggers GenerateFollowFeedItemsJob | Loose to Social — async job (good pattern) |
UpdateGoalEvent | Application Service | Edit or soft-delete a goal event | None | Self-contained |
CheckInHabit | Application Service | One-tap daily habit completion | Creates HabitCompletion, creates GoalEvent, updates streak, calls GamificationService.award_xp(user, 25), triggers GenerateFollowFeedItemsJob | Tight to Gamification — direct award_xp call. Loose to Social — async job. |
UseStreakFreeze | Application Service | Apply a freeze token to cover a missed day | Updates goal streak fields | Self-contained |
CalculateGoalProgress | Query Service | Group events by period, return progress data points | None (read-only) | Self-contained |
BuildGoalSummary | Query Service | Aggregate the current user's dashboard goal summary. Input: current_user (required, guards unauthenticated via fail_with(..., code: :unauthorized)). Returns result.details with exactly total_goals, avg_progress, completed_count, need_attention_count. Extracted from goal_queries.rb#goal_summary, which is now a thin resolver over this interaction — response shape unchanged. | None (read-only) | Self-contained |
Social & Community
| Interaction | Type | Description | Side Effects | Coupling |
|---|---|---|---|---|
CreateCommunity | Application Service | Create a community, make founder admin | Creates CommunityMember with admin role | Self-contained |
JoinCommunity | Application Service | Add user to community as member | Creates CommunityFeedItem, triggers JoinFirstCommunityActionJob | Tight to Gamification — inline badge job trigger |
LeaveCommunity | Application Service | Remove membership | Destroys CommunityMember | Self-contained |
FollowCommunity | Application Service | Subscribe to community notifications | Creates CommunityFollow | Self-contained |
UnfollowCommunity | Application Service | Unsubscribe | Destroys CommunityFollow | Self-contained |
CreateCommunityPost | Application Service | Post in a community | Creates CommunityPost | Self-contained |
AddGoalToCommunity | Application Service | Share a goal into a community | Creates CommunityGoal join record | Reads from Goal Tracking (loose) |
AddCommunitySuggestion | Application Service | Submit community recommendation | Creates CommunitySuggestion | Self-contained |
ToggleFollowGoal | Application Service | Follow/unfollow a public goal | Creates/destroys UserGoalFollow, triggers follow notification | Reads from Goal Tracking (loose) |
ToggleGoalEventEncouragement | Application Service | Like/unlike a goal event | Soft-creates/deletes encouragement, triggers notification and feed item | Reads from Goal Tracking (loose) |
ToggleGoalEventReaction | Application Service | Toggle emoji reaction on a goal event | Soft-creates/deletes/swaps reaction, triggers notification and feed item | Reads from Goal Tracking (loose) |
AddGoalEventComment | Application Service | Comment on a goal event | Creates comment, triggers notification | Reads from Goal Tracking (loose) |
AddPostComment | Application Service | Comment on a community post | Creates comment, triggers ContentModerationService.screen | Self-contained (content screening is fire-and-forget) |
BuildUnifiedFeed | Query Service | Aggregate activity from allies, communities, own goals, notifications | None (read-only) | Reads from Goal Tracking, User Identity, Social (multi-context projection) |
SendPushNotification | Application Service | Deliver push notification to user devices | Sends push via external service | Reads from User Identity (devices) |
AcknowledgeNotification | Application Service | Mark a notification as read | Updates UserNotification.acknowledged | Self-contained |
AcknowledgeAllNotifications | Application Service | Batch mark-all-read, optional team_id scope | Updates UserNotification.acknowledged via update_all | Self-contained |
DeleteNotification | Application Service | Remove a notification | Destroys UserNotification | Self-contained |
BuildNotificationHistory | Query Service | Paginated notification history + unread count, optional team scope | None (read-only) | Self-contained |
User Identity & Profile
| Interaction | Type | Description | Side Effects | Coupling |
|---|---|---|---|---|
ClerkUserSync | Application Service | Find or create local user from Clerk profile | Creates/updates User | Reads from Clerk (external, via ACL) |
UpdateUser | Application Service | Edit profile (name, username) | Syncs to Clerk API | Writes to Clerk (external) |
UpdateUserPhoto | Application Service | Upload profile image | Creates/updates UserPhoto | Self-contained |
StoreUserDetails | Application Service | Save extended profile metadata | Creates/updates UserDetail | Self-contained |
StoreDeviceToken | Application Service | Register push notification token | Creates UserDevice | Self-contained |
RecordSignIn | Application Service | Track sign-in, update streak | Creates UserSignInHistory, calls User#update_streak! which calls GamificationService.award_xp | Tight to Gamification — update_streak! directly calls award_xp |
Gamification & Progression
| Interaction | Type | Description | Side Effects | Coupling |
|---|---|---|---|---|
AcknowledgeAction | Application Service | Mark a badge as seen | Updates UserAction.acknowledged | Self-contained |
UpdateShowcasedAchievements | Application Service | Customize profile badge showcase | Updates user showcase preferences | Self-contained |
RepairStreak | Application Service | Rebuild missed habit completions for a user; triggered after vacation or gap. Loops through missed days, publishes gamification.streak_repaired domain event, awards repair_streak milestone badge | Updates goal streak fields, calls GamificationService.award_xp to debit the repair cost, publishes gamification.streak_repaired | Tight to Gamification — direct award_xp call (XP debit). Loose to Goal Tracking. |
AwardPartnerBonus | Application Service | Award XP bonus when both allied users check in on the same day; called from Gamification::Subscribers after goal_tracking.habit_checked_in event fires | Calls GamificationService.award_xp | Loose to User Identity |
GrantStarterFreeze | Application Service | Issue one free streak freeze token to new users in their first week; triggered from UserIdentity::ClerkUserSync | Updates goal.streak_freezes_available | Loose to Goal Tracking |
AI Coaching
| Interaction | Type | Description | Side Effects | Coupling |
|---|---|---|---|---|
AI mutations (via app/services/ai/) | Domain Service | Generate milestones, refine descriptions, give advice, generate insights | Writes AiUsageEvent via AiMetrics::RecordAiUsageEventJob | Reads from Goal Tracking (conformist) |
Ai::DocsAssistant::AnswerGuideQuestion (Guide Assistant sub-context — see 01 — Bounded Contexts) | Application Service | Answer a public guide question, grounded only in the committed guide corpus; refuses questions out of scope | Writes AiUsageEvent via AiMetrics::RecordAiUsageEventJob | Self-contained — reads no other context |
AI Workforce
| Interaction | Type | Description | Side Effects | Coupling |
|---|---|---|---|---|
AiWorkforce::TriggerRun | Application Service | Create a queued AiRun (deduplicates against existing queued/running runs), enqueue ExecuteRunJob | Creates AiRun, enqueues Sidekiq job | Self-contained within context |
AiWorkforce::UpdateEmployee | Application Service | Update employee configuration (name, schedule, budget, autonomy level) | Writes AiEmployee with promotion validation | Self-contained |
AiWorkforce::ApproveArtifact | Application Service | Operator approval of a pending artifact | Updates AiArtifact.approval_status, enqueues DeliverArtifactJob | Self-contained (delivery is async) |
AiWorkforce::RejectArtifact | Application Service | Operator (or auto via post-filter) rejection | Updates AiArtifact.approval_status | Self-contained |
AiWorkforce::ProcessWebhookResult | Application Service | Apply Agent Runner HMAC-verified webhook payload to AiRun lifecycle | Updates run status, creates AiArtifacts, records cost | External — receives webhook via Webhooks::AiWorkforceController. Direct JSON→model mapping today; candidate for AgentRunnerAdapter ACL. |
Billing
| Interaction | Type | Description | Side Effects | Coupling |
|---|---|---|---|---|
Billing::CreateCheckout | Application Service | Create a Stripe checkout session for a chosen Plan, return redirect URL | Calls StripeService (external) | Writes to Stripe (external) |
Billing::ProcessStripeWebhook | Application Service | Idempotent processor for Stripe webhook events (checkout.session.completed, invoice.paid, customer.subscription.deleted, charge.refunded); stamps stripe_event_id on PaymentRecord and publishes billing.payment_processed | Persists PaymentRecord, publishes domain event | Delegated-to from Webhooks::StripeController after signature verification; Billing::Subscribers mutates user plan in response to the event |
Feedback
| Interaction | Type | Description | Side Effects | Coupling |
|---|---|---|---|---|
Feedback::CreateFeedbackPost | Application Service | Create a feedback post | Persists FeedbackPost | Self-contained |
Feedback::CreateFeedbackComment | Application Service | Reply to a feedback post; sets is_official for staff | Persists FeedbackComment, updates counter cache | Reads User role |
Feedback::ToggleFeedbackVote | Application Service | Add or remove a vote | Persists/destroys FeedbackVote, updates counter cache | Self-contained |
Feedback::UpdateFeedbackPostStatus | Application Service | Admin transitions status (open → planned → in_progress → completed/declined); sets shipped_at when completed | Updates post, enqueues Feedback::NotifyVotersJob | Admin-only; notification is async (good pattern) |
GDPR & Data Rights (Cross-Cutting)
| Interaction | Type | Description | Side Effects | Coupling |
|---|---|---|---|---|
Gdpr::SubmitRequest | Application Service | Public intake for a new GDPR request (export, deletion, or rectification — not "access", see 01 — Bounded Contexts). | Creates GdprRequest(status: 'received'), enqueues Gdpr::SendAcknowledgmentJob (→ GdprMailer.acknowledgment) | Reads User Identity (email); writes to GDPR context |
Gdpr::CascadePreview | Query Service | Admin-only dry-run: per-model counts of what a deletion would touch. | None (read-only projection) | Reads from all contexts (Goal Tracking, Social, Gamification, User Identity, AI, Admin) |
Gdpr::ExecuteDeletion | Application Service | Admin-triggered, step-up-gated. Burns the step-up token, resolves the GdprRequest, delegates the actual cascade to Gdpr::UserDataCascade, then finalizes the request. Mixed hard/soft-delete cascade — not a uniform soft-delete. Hard-deletes goals, mood logs, badges, devices, community memberships, AI memories; soft-deletes/scrubs community posts/comments and the User row itself. | Cascading hard + soft deletes across multiple tables, atomic transaction | Writes to Goal Tracking, Social, Gamification, User Identity, AI, Admin (scrub only) |
Gdpr::DeleteSelfServiceAccount | Application Service | Self-service (OBJ-2183), no admin gate — an authenticated user deletes their own account. Delegates the same cascade to Gdpr::UserDataCascade. | Same cascading hard + soft deletes as Gdpr::ExecuteDeletion; writes its own AdminAction (actor: the user, no GdprRequest) | Writes to Goal Tracking, Social, Gamification, User Identity, AI |
Gdpr::UserDataCascade | Domain Service | Shared cascade — the actual hard/soft-delete steps for a full account erasure, called by both Gdpr::ExecuteDeletion and Gdpr::DeleteSelfServiceAccount. Not itself an Interaction::Base; runs inside the caller's own transaction. | Cascading hard + soft deletes across multiple tables | Writes to Goal Tracking, Social, Gamification, User Identity, AI, Admin (scrub only) |
Gdpr::ExecuteExport | Application Service | Admin-only (super_admin/support). Transitions request to processing, delegates the actual build to a job. | Enqueues Gdpr::BuildExportBundleJob, which builds via Gdpr::ExportBuilder, uploads to GCS, sets export_file_url on the GdprRequest, sends GdprMailer.export_ready | Reads all contexts; writes to GDPR context; sends mail |
Gdpr::RequestSelfServiceExport | Application Service | Self-service export (no admin gate), 24h per-email cooldown. | Same downstream job as ExecuteExport | Same as above |
Admin (Cross-Cutting)
| Interaction | Type | Description | Side Effects | Coupling |
|---|---|---|---|---|
Admin::GenerateWordCloud | Query Service | Word-frequency analysis of goal names (moved from root cross-cutting) | None (read-only) | Reads from Goal Tracking |
Admin::Logged | Application Service (helper/base) | Wraps admin-scoped actions to write AdminAction audit entries | Creates AdminAction | Cross-cutting — used by admin-scoped mutations |
Admin::UpdateUserRoles | Application Service | Grant/revoke AdminRole records for a user | Writes AdminRole, syncs legacy User#admin flag | Writes User Identity |
Admin::UserSearch | Query Service | Paginated admin user search by email/username/role | None (read-only) | Reads User Identity |
Content Moderation (Cross-Cutting)
| Interaction | Type | Description | Side Effects | Coupling |
|---|---|---|---|---|
ReportContent | Application Service | User reports content (post, comment). Creates ContentReport and ContentFlag. | Creates both records in a single transaction | Self-contained. Allowed types: CommunityPost, PostComment, GoalEventComment |
ReviewContentFlag | Application Service | Admin approves or rejects a content flag. Updates flagged content's moderation_status. | Updates flag status and content moderation_status | Self-contained (admin-only) |
ReviewContentReport | Application Service | Admin reviews or dismisses a user report. | Updates report status | Self-contained (admin-only) |
Cross-Cutting / Infrastructure
| Interaction | Type | Description | Coupling |
|---|---|---|---|
Infrastructure::GeneratePublicId | Domain Service | Generate URL-safe base64 token | Infrastructure — used by all PublicRecord models |
UserIdentity::AddMoodLog | Application Service | Create mood check-in | Self-contained (within User Identity) |
UserIdentity::UpdateCoachingPreferences | Application Service | Update a user's AI coaching preferences (frequency, tone, etc.) | Self-contained (within User Identity) |
Note: GenerateWordCloud now lives under the Admin:: namespace; GeneratePublicId now lives under Infrastructure::.
Domain Services (Non-Interaction)
These are service objects in app/services/ that contain domain logic but aren't interactions.
Registration rule (gate g4)
app/services/ holds external adapters (Stripe, Clerk, PostHog, Sentry) and domain services registered in the table below. Registration requires passing a two-part discriminator, both parts required:
- (a) Side-effect-free — no
create!/update!/save!/destroyanywhere in the class. An external adapter is exempt from this part only (e.g.StripeService#find_or_create_customerlegitimately callsuser.update!to cache the Stripe customer id). - (b) Shared by ≥2 caller files, or mirroring a documented cross-stack contract (e.g. a frontend composable of the same shape).
A class meeting the discriminator but missing from this table is a doc-drift finding — register it, don't relocate it. A class in app/services/ that orchestrates writes and isn't an external adapter — fails part (a), not registered, no adapter exemption — is the actual boundary violation and belongs in app/interactions/. This is gate g4's own wording (.claude/skills/objectuve-keystone/references/arch-gates.md:21); registry presence in the table below is the documented exception the gate defers to — the mechanism that closes a "worth documenting, not silently tolerating" audit finding rather than leaving it as ambient allowance.
| Service | Context | Purpose | Called By |
|---|---|---|---|
GamificationService | Gamification | Award XP, check level-up, calculate thresholds | CheckInHabit, User#update_streak!, various jobs |
ClerkJwtVerifier | User Identity | Verify Clerk RS256 JWTs via JWKS | GraphQL context setup (every request) |
AdminStatsService | Reporting | Platform-wide metrics (users, goals, growth) | Admin GraphQL queries |
Ai::CoachService | AI Coaching | AI milestone generation, advice, insights | AI GraphQL mutations (Mutations::Ai::*) |
Ai::SettingsGuard | AI Coaching | Shared fail-closed helper for Settings.ai feature flags — an unknown/renamed flag is treated as disabled rather than letting the error propagate | AI GraphQL mutations (GenerateGoalDraft, RefineDescription, GenerateMilestones, GetInsight, GetAdvice, RefineGoal), AiCoaching::GenerateGoalQuickUpdatesJob, Ai::ServiceClient |
Ai::RateLimiter | AI Coaching | Redis-backed fixed-window rate limiter — would_exceed? (read-only pre-check) and check_and_increment (atomic enforcer) | AI GraphQL mutations (GetAdvice, GenerateGoalDraft, RefineGoal, GenerateMilestones, SuggestGoals), Ai::ServiceClient |
Ai::ModelCatalog | AI Coaching | Allowlist and effective-dated rate schedule for routable Gemini model ids (allowlisted?/routable?/rate_for) | Ai::SetCoachModel, Ai::CoachModel, Ai::Pricing, admin GraphQL queries (admin_queries.rb) |
ContentModerationService | Content Moderation | Entry point for automated screening (profanity filter + async AI screening) | AddPostComment, CreateCommunityPost, and other content-creating interactions |
ContentModeration::ProfanityFilter | Content Moderation | Rule-based profanity detection, creates ContentFlag if issues found | ContentModerationService.screen |
Gdpr::ExportBuilder | GDPR & Data Rights | Builds the export bundle content read across bounded contexts. Respects soft deletions and privacy settings. | Gdpr::BuildExportBundleJob (enqueued from Gdpr::ExecuteExport / Gdpr::RequestSelfServiceExport) |
Ai::AgentRunnerClient | AI Workforce | HTTP client dispatching runs to Agent Runner (Faraday, 202 fire-and-forget) | AiWorkforce::ExecuteRunJob |
Ai::OidcAuth | AI Workforce | OIDC identity-token fetcher for authenticated calls to GCP Cloud Run services | AgentRunnerClient, ServiceClient |
StripeService | Billing | Wrapper around the stripe gem (checkout, subscription, billing portal) | Billing::CreateCheckout, Webhooks::StripeController |
GoalProgressCalculator | Goal Tracking | Shared progress derivation (OBJ-1978) — habit completionRate → roadmap milestone % → currentAmount/targetAmount → not-tracked (percent: nil) precedence. Mirrors ionic_frontend's useGoalProgress composable so backend and frontend can't drift on what "progress" means. Pure computation, no writes. | PersonalAnalytics::CalculateCompletionByCategory, GoalTracking::BuildGoalSummary (reached transitively via goal_queries.rb#goal_summary) |
CommunityAccessPolicy | Social & Community | Team-scoped room access boundary — gates feed/member/badge/post access to a team_id-present community to actual CommunityMembers (team membership alone covers room metadata, not room contents); admins bypass. Pure read/predicate, no writes. | Social::CreateCommunityPost, app/graphql/types/community_type.rb, app/graphql/resolvers/community_queries.rb |
TeamAccessPolicy | Billing (Teams) | Centralizes every "can user X read/mutate Y in Team T" role check (view_team_data/manage_team/manage_billing against owner/admin/member/external_coach roles) — every Teams read and mutate path routes through it instead of inline role checks. Pure predicate methods, no writes. Pre-existing precedent for this registration rule; previously registered nowhere despite ~20 caller files (19 Teams:: interactions + 1 resolver). | Teams::* interactions (CreateTeamInvite, AdjustTeamSeats, PromoteTeamMember, and 16 more), app/graphql/resolvers/team_queries.rb |
Social::ShareableSubject | Social & Community | Resolves and authorizes the celebration a share/preview refers to — owned-and-earned check shared by the preview query and the record mutation, so both agree on exactly what a caller may see or share. Pure predicate/lookup, no writes. | Social::RecordShare, Resolvers::SocialQueries#shareable_moment |
Social::ShareableMomentCopy | Social & Community | Maps a resolved, owned-and-earned share subject onto the fact fields the client's ShareCardData discriminated union expects (ionic_frontend/src/share/types.ts) — despite the name, returns facts only; template strings live client-side in src/share/copy.ts. Pure computation, no writes. Registered under discriminator (b)'s cross-stack-contract clause (one real caller today, mirrors a documented frontend type shape). | Resolvers::SocialQueries#shareable_moment |
Coupling Heatmap
Summary of cross-context write dependencies (reads are generally acceptable):
Goal Tracking ──────► Gamification (2 coupling points)
- AddGoal triggers CreateFirstGoalActionJob
- AddGoal triggers CreateSecondCategoryActionJob / CreateSecondTypeActionJob
(CheckInHabit no longer calls GamificationService.award_xp directly as of v1.12
Phase 53 — it publishes `goal_tracking.habit_checked_in`, consumed by
Gamification::Subscribers. Now event-driven/loose; moved out of this tight count.)
Social & Community ──► Gamification (3 coupling points)
- JoinCommunity triggers JoinFirstCommunityActionJob
- Social::AcceptPartnerRequest#award_xp_to_both calls GamificationService.award_xp (both users)
- Social::AcceptAllyInvite#award_xp_to_both calls GamificationService.award_xp (both users)
(two independently duplicated `award_xp_to_both` definitions, not a shared method —
tracked as a g27 duplicate-rule finding; fixing the duplication won't remove the
coupling itself, just its double-implementation)
User Identity ──────► Gamification (2 coupling points)
- User#update_streak! calls GamificationService.award_xp
- UserIdentity::ClerkUserSync calls Gamification::GrantStarterFreeze (rescued —
failure is logged to Sentry, doesn't fail the sync)
Goal Tracking ──────► Teams (sub-context of Billing) (2 coupling points)
- CheckInHabit enqueues Teams::TeamLeaderboardUpsertJob + Teams::CollectiveGoalContributionUpsertJob
- AddGoalEvent enqueues Teams::TeamLeaderboardUpsertJob + Teams::CollectiveGoalContributionUpsertJobTotal tight coupling points: 9 — 7 flow toward Gamification, 2 flow toward Teams. This is the largest boundary cluster worth decoupling via domain events.
A tenth coupling point used to exist here but was never folded into that count: Critical Path → User Identity (CriticalPath::AwardStimXp wrote directly to stim_* progression fields on User rather than routing through GamificationService). Resolved and migrated per OBJ-2370 (decided 2026-08-09, Option C — see docs/architecture/proposals/critical-path-user-coupling.md): CriticalPath::RecordPlay now publishes critical_path.stim_completed, and Gamification::Subscribers reacts by calling the moved Gamification::AwardStimXp interaction. Now event-driven/loose, same shape as the CheckInHabit exclusion above — it was never counted in the tight total of 9 and still isn't. See 06 — Modularization Guide for how it's staged in the migration order.
Interaction Counts (verified 2026-09-11)
As of this verification pass, rails_api/app/interactions/ contains 204 interaction files across 20 namespaced directories, plus 3 unnamespaced top-level files (dismiss_enneagram_card.rb, submit_enneagram_assessment.rb, dismiss_dashboard_hint.rb) — per find rails_api/app/interactions -type f -name "*.rb" | wc -l and find rails_api/app/interactions -mindepth 1 -maxdepth 1 -type d | wc -l:
| Namespace | Interaction Count |
|---|---|
admin/ | 9 |
ai/ | 9 (includes the nested ai/docs_assistant/ directory — Ai::DocsAssistant::AnswerGuideQuestion, v4.55 Phase 3) |
ai_workforce/ | 9 |
billing/ | 4 |
content_moderation/ | 5 |
critical_path/ | 6 |
demo_data/ | 8 |
feedback/ | 5 |
gamification/ | 8 |
gdpr/ | 6 |
goal_tracking/ | 12 |
infrastructure/ | 1 |
integrations/ | 13 |
onboarding/ | 2 |
personal_analytics/ | 7 |
social/ | 42 |
supporter/ | 1 |
teams/ | 32 |
user_identity/ | 22 |
weekly_digest/ | 3 |
Background jobs live in rails_api/app/jobs/ across 9 namespaced directories (plus application_job.rb at the root) for a total of 25 job files: ai_coaching/ (1), ai_metrics/ (2), ai_workforce/ (5), billing/ (1), content_moderation/ (1), feedback/ (1), gamification/ (8), social/ (5).
BuildUnifiedFeed: Deep Dive
This interaction deserves special attention as the only multi-context query service in the system.
What it does: Assembles a user's activity feed from four data sources, normalizes them into a common shape, sorts by timestamp, and paginates.
Data sources:
GoalEventrecords from accepted allies' public goals (viaUserAlly→Goal→GoalEvent)CommunityPostrecords from communities the user belongs to (viaCommunityMember→CommunityPost)UserFeedItemrecords (badges, encouragements, follows)- User's own public
GoalEventrecords
All queries use a 14-day lookback window and are capped at 50 (allies/community) or 30 (feed items) records per source.
DDD classification: This is a CQRS read model — a projection that reads from multiple aggregates without writing. In a future architecture, this could become a materialized view updated by event subscribers rather than querying live data on every request.
Performance note: Currently does 4 separate queries, merges in memory, sorts in Ruby. If feed performance becomes an issue, consider:
- Pre-computing feed items into a single
unified_feed_itemstable, updated by event subscribers - Using a dedicated feed service (Redis sorted sets, or a purpose-built feed store)
Last updated: 2026-09-13 — registered Social::ShareableSubject and Social::ShareableMomentCopy (OBJ-3684, keystone g4)
Prior update: 2026-09-11