No More Than Three Wishes Per User

Our application is a huge success and now it's time to get some money from it. We want new users to have a maximum of three wishes available. As a user, if you want to have more wishes, you'll probably have to pay for a premium account in the future. Let's see how we could change our code to follow the new business rule about the maximum number of wishes (in this instance, don't consider the premium user).

Consider the following code for a moment. Apart from what was explained in the previous section about pushing logic into our Entities, could the following code work:

class MakeWishService
{
// ...

public function execute(MakeWishRequest $request)
{
$userId = $request->userId();
$address = $request->email();
$content = $request->content();

$count = $this->wishRepository->numberOfWishesByUserId(
new UserId($userId)
);
if ($count >= 3) {
throw new MaxNumberOfWishesExceededException();
}

$wish = new Wish(
$this->wishRepository->nextIdentity(),
new UserId($userId),
$address,
$content
);

$this->wishRepository->add($wish);
}
}

It looks like it could. That was easy — probably too easy. And here we come across different problems. The first is that Application Services must coordinate but shouldn't contain business logic. Instead, a better place is to put the check for the maximum three wishes into the User, where we could have more control of the relationship between User and Wish. However, for the approach shown here, the code seems to work.

The second problem is that it doesn't work under race conditions. Forget about Domain-Driven Design for a moment. What's the problem with this code under heavy traffic? Think for a minute. Is it possible to break the rule of a User to have more than three wishes? Why will your QA be so happy after running some stress tests?

Your QA tries making a wish feature two times and ends up with a user with two wishes. That's correct. Your QA carries on testing the feature. Imagine for a second that they open two tabs in their browser, fill out each of the forms in each tab, and manage to submit the two buttons at the same time. Suddenly, after two requests, the user ends up with four wishes in the database. That's wrong! What happened?

Think as a debugger and consider two different requests getting the if ($count > 3) { line at the same time. Both of the requests will return false because the user has just two wishes. So both requests will create the Wish and both of the requests will add it into the database. The result is four wishes for one User. That's an inconsistency!

We know what you're thinking. It's because we missed putting everything into a transaction. Well, imagine that a user with id 1 already has two wishes, so there's one remaining. Two HTTP requests to create two different wishes arrive at the same time. We start one database transaction per request (we'll review how to deal with transactions and requests in Chapter 11, Application). Consider all the queries that the previous PHP code is going to run against our database. Remember that you need to disable any auto-commit flag if you're using any Visual Database Tool:

How many wishes does the user with id 1 have? That's right, four. How did this happen? If you take this SQL block and execute it line by line in two different connections, you'll see how the wishes table is going to have four rows at the end of both executions. So it looks like it's not about protecting with a transaction. How could we fix this issue? As explained in the introduction, a concurrency control could help.

For those developers more advanced in database techniques, tweaking the isolation level could work. However, we consider that option too complex, as the problem could be solved with other approaches, and we're not always dealing with databases.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset