I know you said, "without a service", but I decided to just list all the various approaches I could think of to solve this problem (not all equivalent):
AR method receives pure external data. Like you said, this is the simplest strategy, but falls a bit short for more complex scenarios. It also couples the AR to a specific kind of song playlist addition rule.
AR method receives a strategy/rule. The AR method is provided with a strategy (that could be impure) to enforce the rule. For instance, you could have a SongPlaylistAdditionRule#check(Song, Playlist)
interface. The Playlist
AR would just call on rule.check(song, this)
to enforce it. The application layer would leverage some kind of rule provider to get the proper kind of rule or would be injected the right one through the IoC container.
A domain service. This one's pretty obvious, not going to explain.
A synchronous policy (event handler) applies in the same transaction. You'd have a SongPlaylistAdditionPolicy
which listens to an event like SongAddedToPlaylist
through an in-memory DomainEventPublisher
allowing the policy to participate/execute in the same transaction. The SongPlaylistAssociationPolicy
would live in the domain. It's similar to #2 & #3, but invoked indirectly.
An eventual consistent policy. Same concept as above, but would execute asynchronously in another transaction. You'd most likely permit the violation, but then inform the user later that the song was removed from the playlist because the policy got violated, or any other kind of compensating action.
A saga/process manager. You can think of this approach as the reservation pattern, where you introduce small strongly consistent transition states that moves towards a final goal. Each state transition is usually processed in it's own transaction and is rolled back through compensating actions in case of a failure. e.g.
a) playlist.add(song)
? fires SongAddedToPlaylist
/throws: checks playlist-specific invariants (e.g. max number of songs in playlist)
b) song.apply(songAddedToPlaylist)
? fires SongAdditionToPlaylistConfirmed
/SongAdditionToPlaylistFailed
: checks song-specific invariants (e.g. how many playlists it belongs to)
It's important to note that of all the above, #1, #2, #3 and possibly #4 are all possibly stale checks, meaning they wouldn't necessarily prevent a rule violation through concurrency, unless you somehow lock all the data that's been read in the given transaction.
#5 could be used in combination of #1-#4 to reduce the number of accidental violations, but also cover scenarios where the rule got violated through concurrency.
#6 is probably the most natural one as from the domain's perspective it's pretty much all ARs. If you do not want to deal with eventual consistency you could always integrate in a way similar to #4 and have all ARs modified in a single TX. In that case you do not have to model the transition states either. Then when you need to scale you move to eventual consistency.
Hopefully this will give you some ideas on how to tackle your specific problem effectively! There's no one fits all solution!
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…