18 KiB
Value Object Candidates Audit
This document lists domain concepts currently modeled as primitives or simple types that should be refactored into explicit value objects implementing IValueObject<Props>.
Priority levels:
- High: Cross-cutting identifiers, URLs, or settings with clear invariants and repeated usage.
- Medium: Important within a single bounded context but less cross-cutting.
- Low: Niche or rarely used concepts.
Analytics
Analytics/PageView
-
Concept:
PageViewId✅ Implemented- Implementation:
PageViewId,PageView,PageViewId.test - Notes: Page view identifiers are now modeled as a VO and used internally by the
PageViewentity while repositories and use cases continue to work with primitive string IDs where appropriate. - Priority: High
- Implementation:
-
Concept:
AnalyticsEntityId(for analytics) ✅ Implemented- Implementation:
AnalyticsEntityId,PageView,AnalyticsSnapshot,EngagementEvent,AnalyticsEntityId.test - Notes: Entity IDs within the analytics bounded context are now modeled as a VO and used internally in snapshots, engagement events, and page views; external DTOs still expose primitive strings.
- Priority: High
- Implementation:
-
Concept:
AnalyticsSessionId✅ Implemented- Implementation:
AnalyticsSessionId,PageView,EngagementEvent,AnalyticsSessionId.test - Notes: Session identifiers are now encapsulated in a VO and used internally across analytics entities while preserving primitive session IDs at the boundaries.
- Priority: High
- Implementation:
-
Concept:
ReferrerUrl- Location:
PageView.referrer,PageViewProps.referrer - Why VO: External URL with semantics around internal vs external (
isExternalReferralmethod). Currently string with no URL parsing or normalization. - Priority: Medium
- Location:
-
Concept:
CountryCode- Location:
PageView.country,PageViewProps.country - Why VO: ISO country codes or similar; currently unvalidated string. Could enforce standardized codes.
- Priority: Medium
- Location:
-
Concept:
SnapshotId- Location:
AnalyticsSnapshot.id,AnalyticsSnapshotProps.id - Why VO: Identity for time-bucketed analytics snapshots; currently primitive string with simple validation.
- Priority: Medium
- Location:
-
Concept:
SnapshotPeriod(as VO vs string union)- Location:
SnapshotPeriod,AnalyticsSnapshot.period - Why VO: Has semantics used in
getPeriodLabel; could encapsulate formatting logic and date range constraints. Currently a union type only. - Priority: Low (enum-like, acceptable as-is for now)
- Location:
Analytics/EngagementEvent
-
Concept:
EngagementEventId- Location:
EngagementEvent.id,EngagementEventProps.id - Why VO: Unique ID for engagement events; only non-empty validation today. Could unify ID semantics with other analytics IDs.
- Priority: Medium
- Location:
-
Concept:
ActorId(analytics)- Location:
EngagementEvent.actorId,EngagementEventProps.actorId - Why VO: Identifies the actor (anonymous / driver / sponsor) with a type discriminator; could be a specific
ActorIdVO constrained byactorType. - Priority: Low (usage seems optional and less central)
- Location:
Notifications
Notification Entity
-
Concept:
NotificationId✅ Implemented- Implementation:
NotificationId,Notification,NotificationId.test,SendNotificationUseCase - Notes: Notification aggregate IDs are now modeled as a VO and used internally by the
Notificationentity; repositories and use cases still operate with primitive string IDs via entity factories and serialization. - Priority: High
- Implementation:
-
Concept:
RecipientId(NotificationRecipientId)- Location:
NotificationProps.recipientId,Notification.recipientId - Why VO: Identity of the driver who receives notifications; likely aligns with identity/user IDs and is important for routing.
- Priority: High
- Location:
-
Concept:
ActionUrl- Location:
NotificationProps.actionUrl,Notification.actionUrl - Why VO: URL used for click-through actions in notifications; should be validated/normalized and may have internal vs external semantics.
- Priority: High
- Location:
-
Concept:
NotificationActionId- Location:
NotificationAction.actionId,Notification.markAsResponded - Why VO: Identifies action button behavior; currently raw string used to record
responseActionIdindata. - Priority: Low
- Location:
NotificationPreference Entity
-
Concept:
NotificationPreferenceId- Location:
NotificationPreferenceProps.id,NotificationPreference.id - Why VO: Aggregate ID; currently plain string tied to driver ID; could be constrained to match a
DriverIdor similar. - Priority: Medium
- Location:
-
Concept:
PreferenceOwnerId(driverId)- Location:
NotificationPreferenceProps.driverId,NotificationPreference.driverId - Why VO: Identifies the driver whose preferences these are; should align with identity/racing driver IDs.
- Priority: High
- Location:
-
Concept:
QuietHours- Location:
NotificationPreferenceProps.quietHoursStart,NotificationPreferenceProps.quietHoursEnd,NotificationPreference.isInQuietHours - Why VO: Encapsulates a time window invariant (0–23, wrap-around support, comparison with current hour); currently implemented as two numbers plus logic in the entity. Ideal VO candidate.
- Priority: High
- Location:
-
Concept:
DigestFrequency- Location:
NotificationPreferenceProps.digestFrequencyHours,NotificationPreference.digestFrequencyHours - Why VO: Represents cadence for digest emails in hours; could enforce positive ranges and provide helper methods.
- Priority: Medium
- Location:
Media
AvatarGenerationRequest
-
Concept:
AvatarGenerationRequestId- Location:
AvatarGenerationRequest.id,AvatarGenerationRequestProps.id - Why VO: Aggregate ID for avatar generation request lifecycle; currently raw string with only non-empty checks.
- Priority: Medium
- Location:
-
Concept:
AvatarOwnerId(userId)- Location:
AvatarGenerationRequest.userId,AvatarGenerationRequestProps.userId - Why VO: Identity reference to user; could be tied to
UserIdVO or a dedicatedAvatarOwnerId. - Priority: Medium
- Location:
-
Concept:
FacePhotoUrl- Location:
AvatarGenerationRequest.facePhotoUrl,AvatarGenerationRequestProps.facePhotoUrl - Why VO: External URL to user-submitted media; should be validated, normalized, and potentially constrained to HTTPS or whitelisted hosts.
- Priority: High
- Location:
-
Concept:
GeneratedAvatarUrl- Location:
AvatarGenerationRequest._generatedAvatarUrls,AvatarGenerationRequestProps.generatedAvatarUrls,AvatarGenerationRequest.selectedAvatarUrl - Why VO: Generated asset URLs with invariant that at least one must be present when completed; currently raw strings in an array.
- Priority: High
- Location:
Identity
SponsorAccount
-
Concept:
SponsorAccountId- Location:
SponsorAccountProps.id,SponsorAccount.getId - Status: Already a VO (
UserId) – no change needed. - Priority: N/A
- Location:
-
Concept:
SponsorId(link to racing domain)- Location:
SponsorAccountProps.sponsorId,SponsorAccount.getSponsorId - Why VO: Cross-bounded-context reference into racing
Sponsorentity; currently a primitive string with only non-empty validation. - Priority: High
- Location:
-
Concept:
SponsorAccountEmail- Location:
SponsorAccountProps.email,SponsorAccount.createemail validation - Status: Validation uses
EmailAddressVO utilities but the entity still storesemail: string. - Why VO: Entity should likely store
EmailAddressinstead of a plain string to guarantee invariants wherever it is used. - Priority: High
- Location:
-
Concept:
CompanyName- Location:
SponsorAccountProps.companyName,SponsorAccount.getCompanyName - Why VO: Represents sponsor company name with potential invariants (length, prohibited characters). Currently only checked for non-empty.
- Priority: Low
- Location:
Racing
League Entity
-
Concept:
LeagueId- Location:
League.id,validateID check inLeague.validate - Why VO: Aggregate root ID; central to many references (races, teams, sponsorships). Currently primitive string with non-empty validation only.
- Priority: High
- Location:
-
Concept:
LeagueOwnerId- Location:
League.ownerId, validation inLeague.validate - Why VO: Identity of league owner; likely maps to a
UserIdorDriverIdconcept; should not remain a free-form string. - Priority: High
- Location:
-
Concept:
LeagueSocialLinkUrl(DiscordUrl,YoutubeUrl,WebsiteUrl)- Location:
LeagueSocialLinks.discordUrl,LeagueSocialLinks.youtubeUrl,LeagueSocialLinks.websiteUrl - Why VO: External URLs across multiple channels; should be validated and normalized; repeated semantics across UI and domain.
- Priority: High
- Location:
Track Entity
-
Concept:
TrackId- Location:
Track.id, validation inTrack.validate - Why VO: Aggregate root ID for tracks; referenced from races and schedules; currently primitive string.
- Priority: High
- Location:
-
Concept:
TrackCountryCode- Location:
Track.country, validation inTrack.validate - Why VO: Represent country using standard codes; currently a free-form string.
- Priority: Medium
- Location:
-
Concept:
TrackImageUrl- Location:
Track.imageUrl - Why VO: Image asset URL; should be constrained and validated similarly to other URL concepts.
- Priority: High
- Location:
-
Concept:
GameId- Location:
Track.gameId, validation inTrack.validate - Why VO: Identifier for simulation/game platform; currently string with non-empty validation; may benefit from VO if multiple entities use it.
- Priority: Medium
- Location:
Race Entity
-
Concept:
RaceId- Location:
Race.id, validation inRace.validate - Why VO: Aggregate ID for races; central to many operations and references.
- Priority: High
- Location:
-
Concept:
RaceLeagueId- Location:
Race.leagueId, validation inRace.validate - Why VO: Foreign key into
League; should be modeled asLeagueIdVO rather than raw string. - Priority: High
- Location:
-
Concept:
RaceTrackId/RaceCarId- Location:
Race.trackId,Race.carId - Why VO: Optional references to track and car entities; currently strings; could be typed IDs aligned with
TrackIdand car ID concepts. - Priority: Medium
- Location:
-
Concept:
RaceName/TrackName/CarName- Location:
Race.track,Race.car - Why VO: Displayable names with potential formatting rules; today treated as raw strings, which is acceptable for now.
- Priority: Low
- Location:
Team Entity
-
Concept:
TeamId- Location:
Team.id, validation inTeam.validate - Why VO: Aggregate ID; referenced from standings, registrations, etc. Currently primitive.
- Priority: High
- Location:
-
Concept:
TeamOwnerId- Location:
Team.ownerId, validation inTeam.validate - Why VO: Identity of team owner; should map to
UserIdorDriverId, currently a simple string. - Priority: High
- Location:
-
Concept:
TeamLeagueId(for membership list)- Location:
Team.leagues, validation inTeam.validate - Why VO: Array of league IDs; currently
string[]with no per-item validation; could leverageLeagueIdVO and a small collection abstraction. - Priority: Medium
- Location:
Summary of Highest-Impact Candidates (Not Yet Refactored)
The following are high-priority candidates that have not been refactored in this pass but are strong future VO targets:
LeagueId,RaceId,TeamId, and their foreign key counterparts (RaceLeagueId,RaceTrackId,RaceCarId,TeamLeagueId).- Cross-bounded-context identifiers:
SponsorIdin identity linking to racingSponsor,PreferenceOwnerId/NotificationPreferenceIdin notifications, and remaining analytics/session identifiers where primitive usage persists across boundaries. - URL-related concepts beyond those refactored in this pass:
LeagueSocialLinkUrlvariants,TrackImageUrl,ReferrerUrl,ActionUrlin notifications, and avatar-related URLs in media (where not yet wrapped). - Time-window and scheduling primitives:
QuietHoursnumeric start/end in notifications, and other time-related raw numbers in stewarding settings and session configuration where richer semantics may help.
These should be considered for future VO-focused refactors once the impact on mappers, repositories, and application layers is planned and coordinated.