Michaël Gallego

This is my blog. What can you expect here? Well... Zend Framework 2, Amazon AWS...

Twitter

Google+

LinkedIn

Github

Last.fm

Architecture best practices n°1 : communicating between services

Writing code is easy, but writing beautiful and maintainable code is much harder. What is even harder is finding resources about how writing beautiful and maintainable code. Trivial examples that can be found in documentation are often too simplistic, and I myself face a lot of difficulties in this area. At the same time, modern frameworks like Zend Framework 2 offer us the power to achieve beautiful code.

This is why I’ve decided to write a new kind of articles in my blog, that would be much more participating posts than my typical articles. I’d love you to share your opinions about those problems, how you do it, why you think a given approach is bad/good… so that everyone can benefit from it.

Concretely, I will share a typical use case I encounter in a real-life example, and outline some solutions I’ve found to this problem with pros and cons. In all cases, I’m not sure myself about which one is better, or if there exist better solution, that’s why I need you =).

The use case

The use case is pretty simple: this website allow its users to enter code that, in turn, credit money to their account. They also have an affiliating program. If a downline user enters 100 codes, then in turn the upline earns 5 €. Simple.

In all solutions, I’ll consider the use of two services: one CodeService (to persist codes) and a UserService.

Solution n°1

The first solution is the most explicit one. This is the CodeService:

class CodeService
{
    public function create(Code $code)
    {
        // Persist the code and check if this user entered more than 100 hundreds code
        $this->objectManager->persist($code);
        $count = $this->countByUser($code->getUser());

        // If count is > 100, then we credit 5€ to the upline user
        if ($count > 100) {
            $uplineUser = $code->getUser()->getUplineUser();
            $uplineUser->incrementCredit(5);

            // Send an email to say it to the upline user
            $message = new Message();
            $message->setTo($uplineUser->getEmail());
            $this->transport->send($message);
        }

        // Flush everything
        $this->objectManager->flush();
    }
}

This is by far the most simplest approach. It’s easy to follow, efficient. The function “countByUser” has been moved into a specific function of the CodeService so that it can be reused in other context.

Pros

  • Easy to follow, everything is explicit
  • The very application-specific service (code service) is tight to a more generic service (user).

Cons

  • We are directly interacting with the user credit and the mail service into a completely different service that deals with code.
  • This method can become super long if we decide to do other things in response to that, like logging it.

I personally like this solution.

Solution 2

Second solution is a small variation of the first one. Instead of incrementing the credit in the CodeService, this is delegated to the UserService:

class CodeService
{
    public function create(Code $code)
    {
        // Persist the code and check if this user entered more than 100 hundreds code
        $this->objectManager->persist($code);
        $count = $this->countByUser($code->getUser());
        // If count is > 100, then we credit 5€ to the upline user
        if ($count > 100) {
            $this->userService->incrementFromDownlineUser();
        }

        // Flush everything
        $this->objectManager->flush();
    }
}

Because the “incrementFromDownlineUser” is specific enough, then the mail sending is also moved to this method instead.

Pros

  • It’s still easy to read and follow.
  • The logic of dealing with the user credit has been moved… well… to the UserService.

Cons

  • The User service is now cluttered with a method that may be used only in this context (only called the create method of CodeService). If we multiply those methods, the service may become unreadable.
  • It may be less efficient because the incrementFromDownlineUser will likely perform a “flush” itself, so we will have two queries instead of one. The solution would be to wrap the create method in the CodeService by a transaction (Doctrine is smart enough to detect nested transactions and will not commit the result even if the incrementFromDownlineUser does one flush).

Solution n°3

The solution n°3 relies on events:

class CodeService
{
    public function create(Code $code)
    {
        // Persist the code and check if this user entered more than 100 hundreds code
        $this->objectManager->persist($code);
        $count = $this->countByUser($code->getUser());

        // If count is > 100, then we credit 5€ to the upline user
        if ($count > 100) {
            $this->eventManager->trigger('codeCreated', array(
                'code' => $code
            ));
        }

        // Flush everything
        $this->objectManager->flush();
    }
}

 Pros

  • The CodeService is now completely independent and has no dependency on any other services: it just triggers an event.
  • It is super flexible, because you can have a LoggerService listens to this event, without explicitly having a logger in your CodeService.
  • It makes you feel like a pro.

Cons

  • It’s slower.
  • It makes the workflow super hard to follow. Who are listening to this event? Where?
  • It now introduces a dependency from the UserService to the CodeService because the first one needs to register a listener (ok, it can be in the Module.php class instead, but it makes the workflow even harder to follow to my opinion).

Solution n°4

The solution n°4 would be a hybrid solution. It would be the 1st solution, but it would also trigger an event, so that services that log could still occur.

Conclusion

What’s your opinion about this? Which solution would you prefer for this use-case, and why? Or may be you have better ideas than me… Comment! :)