Usecases, entities and mixed storage

89 views
Skip to first unread message

Liviu Gelea

unread,
Oct 24, 2023, 2:01:06 PM10/24/23
to Clean Code Discussion
Hello. Let me first humbly thank you for creating this group. Joining this and being able to ask questions feels to me akin to taking advantage of people's expertise. I am severely thankful to anyone  willing to shed some light on some issue I'm struggling with.

Premises:
* Karaoke party management web application
* I want to allow the flexibility to either use Runtime (in memory) storage OR request-based database persisted data.

My problem is that entities already seem like runtime storage. My application works with concepts such as "Party" and "PlaylistItem".  Let's just imagine Parties contain many PlaylistItems and each PlaylistItem contains a Song.

As a business rule, we know Parties can enqueueSong(Song, Singer) which means wrapping the song and singer into a PlaylistItem and appending it to a private variable. Now the key point here is that the PlaylistItems are stored on the Party which serves as an aggregate root.

Now in order to manage Party operations (mostly CRUD based) I created a PartyStorage interface with two implementations: RedisPartyStorage and RuntimePartyStorage.  My thinking was that I could persist the entire aggregate. But this started to look like a bad idea given that Redis has very efficient list operations. I want to be able to enqueue just a song without saving the entire aggregate.

Is enqueueing the song the use case or is persisting the enqueued song the use case?
Should I do
$this->partyStorage->enqueueSong($playlistItem, $partyId);
or
$party->enqueueSong($playlistItem);
$this->partyStorage->store($party);
I tend to like the first version because of storage performance but I think the second is more Object Oriented and has less risk of making prior Party instances stale. Also  I thought of other ideas but none are great:
* Injecting partyStorage inside of the Party object in order to manage persistence looks too much like Active Record.
* Event sourcing so that storing() the aggregate itself does not persist everything but instead replays the changes to the playlist and such
* Optimistic locking, using the first variant of my code, but ensuring stale Party instances cannot override newer changes.

I feel like I'm a totally wrong track. Like I'm confusing runtime storage to entities.

I appreciate any advice. Thanks.

Łukasz Duda

unread,
Oct 26, 2023, 5:07:33 AM10/26/23
to Clean Code Discussion
Hi,
Storage is most of the time technical concern while use case is more often about business requirements. So the use use case could be "add song to queue".
Why do you need party to manage playlist? Are there any consistency rules enforced by the aggregate root other than the order of songs? There isn't single correct object design. Each decision has pros and cons.
Both solutions could work. I would start with the simplest. Why do you expect performance issue? Do you expect 1000 people managing single party at the same time?
Sure you can use Domain-Driven Design aggregate and event sourcing, but you say it's mostly CRUD, so I wonder if it's worth such complexity.
I think I would start with simple use case (command) that uses repository to get party (snapshot), add song to queue and then save party (changed snapshot and it's version for optimistic concurrency control), but it's a question of your current architecture drivers (performance, development cost/time, learning new skills, simplicity, maintenance cost).

Regards
Łukasz

Liviu Gelea

unread,
Oct 27, 2023, 3:53:54 AM10/27/23
to Clean Code Discussion
Hello and thanks for your answer.
Let me explain my train of thought. These are not excuses but my thought process which may be wrong. It' why I asked this question.
One Party has a single playlist which I made as a simple hashmap property. You could even say "Playlist" is another  possible name for "Party" but I liked the idea of a "Party" having a "Host" rather than a " Playlist" having an "Owner" so I chose the former. This makes the Party class somewhat have a dual responsibility: it both stores playlist AND party settings such as whether it's private (you need a link to join) or listed in the public party list. I can separate the two which may solve the issue
The reason I chose to $party->enqueueSong() is due to my understanding of our dear Uncle Bob's "code should read like a sentence". I'm experimenting with TDD I have no prior experience with and it just felt like a natural way to organize things.

function testUsersCanDeleteOwnSongsFromParty()
{
$partyCollection = new RuntimePartyStorage();
$george = new ApplicationUser($partyCollection, GUID::generate());
$deanna = new ApplicationUser($partyCollection, GUID::generate());
$lucretia = new ApplicationUser($partyCollection, GUID::generate());
$partyOfGeorge = $george->throwParty();

$songOfDeanna = new YoutubeSong(GUID::generate(), "CCC");
$playlistEntryOfDeanna = $deanna->enqueueSong($partyOfGeorge->id, $songOfDeanna);

$songOfLucretia = new YoutubeSong(GUID::generate(), "AAA");
$playlistEntryOfLucretia = $lucretia->enqueueSong($partyOfGeorge->id, $songOfLucretia);

$this->expectException(UserDoesNotOwnSong::class);
$lucretia->deleteSong($partyOfGeorge->id, $playlistEntryOfDeanna);
$this->assertContains($playlistEntryOfDeanna, $partyOfGeorge->listSongs(), "Expected Lucretia to fail deleting someone else's songs");

$deanna->deleteSong($partyOfGeorge->id, $playlistEntryOfDeanna);
$this->assertNotContains($playlistEntryOfDeanna, $partyOfGeorge->listSongs(), "Expected Deana to be able to delete own songs");

$george->deleteSong($partyOfGeorge->id, $playlistEntryOfLucretia);
$this->assertNotContains($playlistEntryOfDeanna, $partyOfGeorge->listSongs(), "Expected Party owner to be able to delete Lucretia's song even if he/she does not own the song itself.");
}
Do you see some issues In how I TDDd this?
I know the class ApplicationUser does not look like a "UseCase" but its methods are somewhat usecase-ish. Am I obsessing over the "read like a sentence" too much?

My "performance" argument is very weak. A better explanation would be that I don't want to bundle more data than the application needs to get the job done. One Karaoke night usually has about 40 songs. I don't see why I would overwrite the entire playlist just because the party owner decided to move a song up the list a few positions and in my mind this is more related to ensuring consistency than performance itself.

My goal with this project is to learn TDD, DDD and Clean architecture while providing my friends with an improved karaoke experience. I can focus on any principles but my main driver has always been maintainability.
I am currently in a bad place. I have 15 years of experience developing applications but I am on the lowest point on the Dunning krueger graph. It's like I've learned wrong things all my life so now I have choice paralysis and I don't seem to be able to finish projects in time. I need to get my principles in check so I can be productive again.

Łukasz Duda

unread,
Nov 12, 2023, 1:30:37 PM11/12/23
to Clean Code Discussion
Hi,
I could suggest a few things...
You can divide the testUsersCanDeleteOwnSongsFromParty into three tests (single assertion rule - don't act, assert, act, assert):
- testPartyOwnerDeletesSongAddedByPartyParticipant
- testPartyParticipantCanDeleteOwnSong
- testPartyParticipantCantDeleteSongAddedByAnotherPerson
You don't need assertion message then.
You can also test edge cases, for example try adding the same song twice, remove song from another party, remove song from empty party collection.

I would also use role names, because they are more meaningful than usernames. For instance:
$partyOwner instead of $george
maybe PartyParticipant instead of ApplicationUser
InMemoryPartyStorage or InMemoryPartyRepository instead of Runtime, because database works also at runtime

Read like a sentence is a good rule and you always have to overuse something, when learning, before you get a feel what work's for you and what not.

If your goal is to learn you can try a few different styles and compare results then. For example you can compare readability and performance for:
1. simple representation - overwrite whole party
2. optimized performance - separate party from song collection use cases (maybe some use cases don't need to to access whole collection)
3. auditability - represent party as a stream of events (event sourcing)

Programming is already difficult, so my preference is almost always simplicity (YAGNI and KISS rules), but it's good to verify choices and my favorite way is to build prototype of each solution and compare.

Good luck :-)
You can share if you get some interesting results of such comparison.

Łukasz Duda

unread,
Nov 13, 2023, 4:09:53 AM11/13/23
to Clean Code Discussion
Lately I've been seeing a lot of interesting style that fits well with DDD.
Here you will find an example of CQRS with elements of tactical DDD and messaging discovered during event storming. This style less complex than event sourcing.
https://github.com/devmentors/Pacco.Services.Availability
https://github.com/devmentors/Pacco
Reply all
Reply to author
Forward
0 new messages