Videa Blog

Review of the Advanced Web Application Architecture

Svaťa Šimara  
  • This book will teach you how to structure applications that will last for years.
  • It is full of code examples that are easy to understand and follow.
  • I recommend this book, it is great.

Book

What is the Book About

Matthias shows how to focus on the business needs, on use cases and how to write it down into the code. He calls this code "core code". The core code is decoupled from databases, frameworks or libraries. This is the general idea of the book - isolate the code containing business logic.

Part I explain why should we focus on separating core code from surrounding infrastructure code and step by step shows how to achieve that. This separation leads to readable, maintainable, testable, extendable code that will survive at least years. Part I is also fairly unit tested and explains how to write proper - fast and meaningful - unit tests.

Part II puts concepts from Part I into context, this is actually the architecture part. Matthias explains here how to organize layers, what is hexagonal architecture and how to achieve it. One whole chapter is dedicated for testing - what tests write for what layer. Yes, there are plenty types of tests, not unit tests only.

Writing Style & Code Examples

The book is using easy English and anyone who can read this review can read the book. Matthias seamlessly leads reader from known but messy code to new well-structured approach. Difficult topics like entities, repositories, ports, adapters, ... are explained so easily that most of the readers will understand them immediately.

This is a book for programmers, about half of the book are code examples. Topics might be difficult to understand, but once we see the code in action, we immediately understand it.

Code examples speaks itself, the code is full of classes and interfaces with clear behavioral responsibility. Definitely matured OOP worth following.

Personal Impression

The power of suggested architecture is that it leads to software that is clear, testable and maintainable. I just agree.

I surprisingly iterated over years to the same architecture. So this book unfortunately confirms my architecture style and next time I'll face such challenge I'll be more resistant to different approach/architecture.

I recommend reading this book to everyone who

If you aren't interested in application architecture much, but you somehow have a copy of the book in your hands than I strongly recommend reading chapters 9.1 and 9.3 first. Really, just these chapters and then continue from 1. I was missing a motivation, the why until these chapters, so don't worry and start with them.

Matthias doesn't use prefixes/suffixes in code examples like *Interface, and it makes me very satisfied.

The book has almost 400 pages, but I was able to read it fairy quickly due to easy english and enormous amount of code examples.

Ideas that Made My Day

Chapters in Detail

I like most of the book, sometimes I disagree and sometimes I'm missing important information. You can find here couple of my notes and confront them with your point of view.

✖ ORM Mappings in PHP Annotations (chapter 2.5.1)

Matthias suggests writing ORM mapping directly to PHP annotations

/**
 * @ORM\Entity
 * @ORM\Table(name="orders")
 */
final class Order
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue
     */
    private $id;
}

Including ORM mapping in the core code has disadvantages/problems

I strongly suggest writing ORM mapping in separate files, like XMLs.

✔ Don't Use Active Record (chapter 2.8)

Matthias discourages us from using active record design pattern.

This technique has several disadvantages, for me the most important are

✔ Use Read Model (chapter 3)

Matthias introduces read models and view models without any questions, and I like it. We need more view models and thinking that reading is very different from writing.

✔ Hide Low Level Details (chapter 3)

Matthias suggests to hide low level implementation like database queries behind abstractions. Sure, but why?

The reason for me is that high-level core code is easy to understand for new programmers (or for me two weeks later). It's easier to maintain the logic if we are not overrun with implementation details.

❓ How to Solve Naming Collisions (chapter 3)

The book doesn't deal with possible naming collisions.

We have an Order as write model, and we may have Order in customer account, Order in API, Order send by email, ... How to deal with multiple concepts that naturally have the same name?

I wouldn't recommend using invented names because than it would be difficult to understand the concept. So I'd like to know what Matthias suggests.

I personally tried to distinguish these classes by namespaces, but result is messy. Once I look for a class, I have several occurrences and have to think about namespaces.

A solution that works for me is to prefix read models by the purpose. So I'd have Order - write entity, and read models - CustomerAccountOrder, ApiOrder, EmailOrder. But is it good?

✖ Naming "Application Services" (chapter 4)

Application services are classes that encapsulate domain use-case, are isolated from infrastructure and belongs to core code. One such service could be OrderEbookService with method order(). The concept makes sense.

I have a problem with the naming.

Instead, I'd like to suggest naming that fits better and is even used later in the book - use-cases.

✖ Using Mocks in Tests (chapter 5.7)

Matthias suggests to use mocks in tests for external dependencies like Translator.

interface Translator {
    public function trans(string $message, string $locale): string;
}

$translator = $this->createMock(Translator::class);
$translator->expects($this->once())
    ->method('trans')
    ->willReturn(
        function (string $message, string $locale): string {
            return $message . '(translated)';
        }
    );

In this situation when we have an interface, I'd rather use anonymous class (or implementation for tests).

$translator = new class implements Translator {
    public function trans(string $message, string $locale): string {
        return $message . '(translated)';
    }
};

Benefits

The anonymous implementation of course doesn't support checks like how many was called. If we have such needs, we shall think more what are we testing and what do we expect the stub/mock does. So there might be situations where mocks are more practical than anonymous implementation.

✖ Misinterpretation of Inversion of Control (chapter 5.9)

Matthias says that declaring required constructor arguments is called inversion of control, and we should never use service locator.

The statement is unfortunately wrong, inversion of control != constructor injection.

Inversion of control is a concept that says we shouldn't construct dependencies by ourself, and we should require them. One implementation of inversion of control is a service locator and one implementation is a constructor dependency injection (and there are a couple of more implementations).

I agree with the Matthias that dependency injection is better than service locator, but inversion of control isn't dependency injection.

✔ Behavior of Objects (chapter 7.4)

"Value objects should offer no behavior that hasn't been explicitly enabled and designed for your use case"

One of top highlights of the book.

I'd like to extend the statement to "any object". Then the code is easy to understand because it does the job we need, no noise, no unused behavior.

This highlight reminds me frameworks/libraries that forces us to extend from base classes while these base classes have tens of methods. No, please, no.

✖ Validation (chapter 8)

This chapter starts well - value objects and entities are always valid. Unfortunately the whole chapter feels unfinished, it is for me the weakest chapter in the book.

Multiple Validation Errors

Matthias suggests constructing value objects in validation layer to catch exceptions and perform validations, and then construct them again in core code for business logic. We can find this code listing 8.11 (chapter 8.3) and it isn't the best and even Matthias isn't satisfied with it. We have to double check inputs - once in the validation layer and once in the core layer. It is double effort for runtime, but also double effort for writing the code that may result in an inconsistency.

Then Matthias recommends that UI shouldn't allow us to make mistakes, and therefore we don't have to write such complicated code anymore.

Matthias conveniently forgets about API calls and following chapters don't provide viable solution.

Translatable Exceptions

Matthias suggests to use translatable strings (chapter 8.4) in domain exceptions and translate these string in UI layer to user readable error message. Core code suddenly have an information that is useful only for the UI (not core layer). So we mix layers, and that is a bad idea.

Possible solution is to name the exception class by the reason. This is meaningful in core code - we'll need such information at least in tests, and exception name can be still translated in UI layer.

Different Exceptions

The book unfortunately doesn't distinguish runtime (catchable) and logic exceptions (chapter 8.4).

Logic exception is an error caused by a programmer, like InvalidArgumentException or TypeError. These exceptions should never be caught and translated to user errors because user can't do anything about it.

Runtime exceptions is something that can occur only in runtime, like ValidatorException, UnexpectedValueException or IOException. Users can usually fix problems by themselves so such exceptions should be translated to user readable error message.

Double Effort Solution

We touched this topic in Multiple Validation Errors and we'll combine it with commands.

Matthias suggests to

I have the problem with double creation of value objects.

To solve this problem, I suggest passing value objects to commands. It solves all problems because we create value object only once. Therefore, there is no runtime overhead, and can't lead to inconsistency.

✔ Structure of Application

Matthias suggests following structure of application

Yes, this is the way to structure application that isn't framework-centric, but core code centric. An application that should survive surrounding technologies.

MVC is really not enough for a domain focused application that should last years.

✖ Connecting UI and Application Service Layers (chapter 13)

The whole chapter focuses on connecting layers. The only thing that bothers me is connection between UI and Application Service layers. Matthias suggests two solution for this topic.

Create an interface to the application layer with all necessary methods. Eg.:

interface ApplicationIterface {
    public function order(/* parameters */): OrderId;
    public function listProducts(): array;
    public function authenticateUser(/* parameters */): UserId;
    // ...
}

This solution horrifies me when I imagine how many methods will be there. Not only the amount of methods is problem, also every controller using this interface will use just one (or few) methods, but will always receive all of them.

Another solution uses commands and command handler:

class CreateOrderCommand {
    private array $products;
    // parameters, constructor, getters
}

class OrderService {
    public function handle(CreateOrderCommand $command): OrderId {
        // behavior
    }
}

interface CommandBus {
    /**
     * @param mixed $command
     * @return mixed
     */
    public function handle($command);
}

class OrderController {
    private CommandBus $commandBus;
    public function createOrder(Request $request): Resposne {
        $command = new CreateOrderCommand(/* ... */);
        $result = $this->commandBus->handle($command);
        // $result is mixed ?!
    }
}

This solution is bit different. Clean in separation, nice for tests, but we have no idea what the command bus returns or throws. We can do some tricks like assert() but the code doesn't provide information to the reader what is really returns.

Return value could be solved by command handlers that don't return anything.

class OrderService {
    public function handle(CreateOrderCommand $command): void {
        // behavior
    }
}

This works. One problem here could be dealing with generated ID, but this has a solution as well.

class OrderController {
    private CommandBus $commandBus;
    private OrderIdGenerator $orderIdGenerator;
    public function createOrder(Request $request): Resposne {
        $orderId = $this->orderIdGenerator->generate();

        $command = new CreateOrderCommand($orderId, /* ... */);
        $this->commandBus->handle($command);
    }
}

The problem with thrown exceptions is still here. We have no idea what exception the handler may throw. I don't have a solution for that.

✖ Contract Testing (chapter 14.3)

Testing adapters (eg. repositories) should be done by testing heir public methods only. This idea is beautiful, but may not test enough.

interface Repository {
    public function get(OrderId $orderId): Order;
    public function save(Order $order);
}

If we test a DoctrineRepository that implements this interface just by the contract, we may end up with the same instance that we've stored.

$orderId = new OrderId();
$order = new Order($orderId, /* parameters */);
$this->repository->save($order);

$foundOrder = $this->repository->get($orderId);
assert($foundOrder === $order); //true

This is because Doctrine holds entity map, and when we request an object that is already in memory, Doctrine returns it.

This means the test didn't test entities loading at all. So if we forget to map a new property into a database, this test may not discover it.

A test ensuring loading object have to clean the EntityManager.

$orderId = new OrderId();
$order = new Order($orderId, /* parameters */);
$this->repository->save($order);

$entityManager->clear();

$foundOrder = $this->repository->get($orderId);
assert($foundOrder === $order); //false! Not the same instance

Such a test isn't contract test anymore.

I solved this issue by contract test that defines abstract method for flushing, and such flushing is defined in a concrete technology test.

✔ Gherkin Scenario Based Testing (chapter 14.5)

Matthias provides how-to tutorial for Gherkin scenario based tests.

Such test looks like

Given the user has not ordered yet
When the user adds a book with price 100€ into the cart
Then the shopping cart total is 90€

This is a new approach for me, and I'm looking to use in next project. Because it has several benefits.

By the way, scenarios tests the core code only, so tests are still fast.

✔ Development Workflow (chapter 14.7)

Matthias focuses on communication between programmers and other company members. That's in my opinion the most important aspect to deliver software that has value and lasts.

This is one of the best chapters in the book which also describes where to start with development and how to proceed.

➕ Topics I Miss in the Book

Thank You

Matthias, thank you for the book, it is great.

The detail review may look like I disagree with lots of things, but it's not true. I wanted to explain why do I disagree and suggest an alternative. I'm open for discussion!

I in fact agree with most of the book, and when I skipped a chapter in the review it means I have nothing to add, I just agree.

Contact

Do You want to improve your architecture?. Hire me, I'll help You svatasimara.cz