I'm developing an application following Domain-Driven Design (DDD) principles, and I need help modeling the following requirements:
Main Purpose:
An application to control sound Players (Speaker) within a HouseHold through RemoteControl.
Think of a RemoteControl as a sound mixer with 1-N channels, Where each Channel controls different Player.
Relationships:
- Each
Playeris associated with a singleHouseHold. - A
HouseHoldcan have one or manyPlayers. RemoteControlis not associated with aHouseHold.- Each
RemoteControlhas a list ofChannels. - Each
Channelis uniquely identified by its position (index) within theRemoteControl.
Channel-Player Pairing:
- A
Channelcan be paired with aPlayer. This pairing designates that theChannelcontrols the associatedPlayer. - A single
RemoteControlcannot be paired with the samePlayeron multiple channels. - However, different instances of
RemoteControlcan be paired with the samePlayer, once each. - The goal of this pairing is to locate a
Playernot by its ID (PlayerId) but through theRemoteControl-Channelpair.
Remote Control Details:
- The
RemoteControlis essentially a simple IoT device with an array of volume inputs. - It is unaware of the players and their IDs.
- Each request from the client includes
remoteControlId,channelPosition,eventTypeandvalue. - Using the pairing information, the application should identify the associated
Playerand update its state accordingly.
This is what I came up by now:
The following entities with the intention of defining Player, RemoteControl, and HouseHold as roots.
Player:
- id
- houseHoldId
- name
- volume
RemoteControl:
- id
- channelsCount / listOfChannels
HouseHold:
- id
- Map<PairdChannel,String> map/List pairs
PairedChannel: (V/O)
- remoteControlId
- channelPosition
Pair:
- id
- playerId
- pairedChannel
And the relevant use-cases in app services might be:
HouseHoldAppService:
function pair(houseHoldId, remoteId, channelPosition, playerId) {
houseHold = repo.getById(houseHoldId)
if (!houseHold) throw...
remote = remoteService.getById(remoteId);
if (!remote) throw
player = playerService.getById(playerId);
if (!player) throw
pairedChannel = new PairedChannel(remote.id, channelPosition);
pair = Pair.create(pairedChannel, player.id);
houseHold.add(pair);
houseHoldRepo.add(houseHold);
}
RemoteControlService:
function changeVolume(remoteId, channelPosition, value) {
pair = houseHoldService.findPair(remoteId, channelPosition);
if (!pair) throw
playerAppService.changeVolume(pair.playerId, value);
}
PlayerAppService:
function changeVolume(playerId, value) {
player = repo.getById(playerId)
if (!player) throw
if (!player.changeVolume(value)) return
repo.store(player)
dispatch(player.dequeue())
}
An high-level flow of the app:
- Dedicated driver emit new event when speaker is found (with speaker data + houseHoldId)
PlayerAppServicecatches this event, and create newPlayerdomain object.HouseHoldAppServicecatches thePlayerCreatedEvent, and if theHouseHoldis not exists - it creates it.- Another dedicated driver emits event when Remote control is connected.
RemoteControlAppServicecatches this event, and create newRemoteControldomain object- The user pair
ChanneltoPlayerby using API such: url:/household/:id/pair/body:{ remoteId, channelPosition, playerId } - The Remote can change the volume of the paired
Playerusing API such: url:/remotecontrol/:id/channel/:channel/changeVolume/:volume - The dedicated speaker driver can always emit events to change the volume of the
Playerby it's id. For example:event: changeVolume, data: { playerId, volume }
I'm seeking advice and/or feedback on how to structure these relationships and implement the pairing mechanism effectively within the context of Domain-Driven Design. Any insights, advice, or code examples related to aggregate design would be appreciated.