[en] Deleteable Package Structure,
There are several ways to structure projects at larger scale.
This blogpost describes one architectural style I like to use in medium / larger applications.
To make it easier to reference this style, i’ll start calling it DPS
(Deleteable Package Structure
).
The examples are written using Symfony, but DPS
is framework agnostic.
DPS vs. Hexagonal Architecture.
I would describe DPS as an anemic SOA approach to Hexagonal Architecture. From my point of view Hexagonal Architecture is more about DDD. DPS focus more on building different implementations and getting rid of old code. DDD focus on building a solid domain, DPS focus on building a delete / replaceable layer for infrastructure intensive projects with huge change rates AND solid constraints for your domain. DPS is much more service oriented.
Goals Of DPS?
DPS
is designed for agile teams, that are building in house projects with a duration of more than 4 months.DPS
focus on building a lightweight project with the possibility to delete as much complexity as possible.DPS
suits well, if the core domain is well known but the project requirements change frequently.DPS
ensures that rewrites of core functionality can be made within sprints.DPS
highly tries to prevent big ball of muds, even with short sprints and indecisive product owners.DPS
scales well in terms of, what kind of tasks should be parallelized and when the team should come together.DPS
ensures fast and efficient code reviews.
Basic Structure
DPS
encores 3 different kinds of Packages
.
| Package Type | Responsibility |
| --------------------- |-------------------------------------------- |
| Project Package | Project specific files / sourcecode. |
| --------------------- |-------------------------------------------- |
| Domain Package | Interfaces and Abstract Classes all |
| | Packages can refer. Defines the public API |
| | of all data structures and services |
| --------------------- |-------------------------------------------- |
| Package | Implementations of Private and |
| | Public Services. Public Services must |
| | implement and return an interface provided |
| | by the Domain Package |
| --------------------- | -------------------------------------------- |
Project Package
DPS enforces one or many Project Packages
.
The Project Package obtains all code for all endpoints (Routing Informations, Controllers, Webservices, Templates, …).
In a typical Symfony Application this could be the AppBundle
.
Most projects will start with one Project Package
.
The Project Package
is allowed to contain:
Framework Specific Functionality
It’s totally fine to couple the framework with the Project Package.
One of the goals of DPS
is to avoid coupling whenever it makes sense.
API Endpoints and Controller
Every endpoint of the Application is provided by the Project Package
.
The related controllers must only use classes / resources from it’s own Project Package
namespace or the Domain Package
namespace.
The Project Package
is not allowed to use any class provided by a regular Package
.
Example
<?php
//....
use [Project]\Domain\Service\ProductServiceInterface; // 1*
use [Project]\Domain\Service\CategoryServiceInterface;
use [Project]\Domain\Domain\ProductInterface; // 2*
use [Project]\AppBundle\API\ProductTransformer; // 3*
class ProductController {
// ....
function __constructor(
ProductServiceInterface $productService,
CategoryServiceInterface $categoryService,
ProductTransformer $productTransformer
) {
// ...
}
public function indexAction() {
/** @var $products Products[] */ // 2*
$products = $this->productService->findAll();
return [
'products' => $productTransformer->tranform(
$products
)
];
}
}
(1*) it’s a good practice if controllers are just using interfaces that
provided by the
Domain Package` and framework related code.
(2*) The Project Bundle
can provide Public Services
, it’s also fine if
the Project Bundle
makes use of it’s own services.
This seems to be a good idea if the service is highly project specific.
Templates
Every template is provided by the Project Package.
Wire Public Services And Provide a Domain Specific Service Name.
Packages
are providing services with knowledge of the technical implementation
and the use case they are used for.
For Example these are valid Package
names:
- ProductMysqlBundle
- ProductDoctrineBundle
Consider the domain of the project is a classic shop.
In this case the Project Package
is responsible for providing a product_service
.
Instead of implementing some kind of product_service
the Project Package
can
use Public Services
provided by other Packages
.
In case of a symfony application, the Project Package
can use the alias functionallity,
to wire any service to the required product_service
.
<service id="product_service" alias="[project].product_mysql_bundle.product_service"/>
The mysql
keyword becomes an implementation detail, other packages can now rely
on the product_service
because it’s wired by the Project Package
, and the product_service
implements an interface provided by the Domain Package
.
Changing the implementation of the product service, is up to the Project Package
.
Different Project Packages
could use the same packages in other situations,
or replace Packages
with other Packages
. It’s also totally fine to A / B Test
different Packages
.
Public / Private / Tagged Services
It’s totally fine if the Project Package
defines Public Services
and
Private Services
. This is useful if the logic behind the service is quite easy and
specific to the project. For further reading, study the Public / Private Service Part of the Packages
,
the restrictions are the same.
Tips
The Project Package
should be as small as possible and can become
that huge that rewriting isn’t possible anymore. Keeping the Project Package
as simple as possible is a good idea.
Domain Package
DPS
enforces a special Domain Package
in a distinct domain namespace.
tree .
|-- Domain
|-- ProductInterface.php
`-- CategoryInterface.php
`-- Service
|-- ProductServiceInterface.php
`-- CategoryServiceInterface.php
The Domain Package
must be framework-agnostic.
The Domain Package
must only contain:
Public Service Interfaces
Interfaces that are designed for stateless, framework-agnostic services.
Domain DTO Interfaces
Every data structure that is used to share data between Packages
must be provided by
as in interface by the Domain Package
.
For example, if the core domain is a shop, the Domain Package
provides an interface for a product,
if the product contains variants, or other subclasses, they must be provided as interfaces, too.
Domain DTO Abstract Classes
Abstract classes with boilerplate data structures. Use abstract classes wisely and rare, prefere interfaces over abstract classes whenever possible.
Tags
The Domain Package
can define some Service Tags
. Tags can be used by the Project Package
or
any other Package
to wire services together.
Tips
Every team member should know (at least) most of the domain interfaces.
Interfaces in the Domain Package
should be defined wisely and with care.
Focus on facts of your domain, not on technical decisions.
Design the interface as small as possible. Design nothing you don’t need.
By reading the service definition a new team member knows every important service, this ensures
that new team members don’t get overstrained.
Packages
Different functionality should live in different Packages
.
Every Package
must only use classes / resources from it’s own Package
namespace or the Domain Package
namespace.
Packages
should not contain interfaces or any abstractions.
Package
are responsible for providing one, efficient solution.
DPS
prefers rewriting Packages
over building a big ball of mud.
Packages
are designed to be small, efficient, easy to understand and maintainable by a small team.
Packages must only contain:
Private Services
Every Package
is allowed to contain services / classes that are used internally to structure the Package
.
A Private Service
must not be used outside of the Package
namespace.
By default every service is a Private Service
.
Public Services
A Private Service
becomes a Public Service
if it implements an interface provided by the Domain Package
.
A Public Service
must return native PHP Types (DateTime, Array, String, Int, …) or instances that implement
an interface in the Domain Package
.
Public Services
can be wired together by the Project Package
.
The Project Package
and any other Package
can make use of any Public Service
by using the interface the Public Service
implements from the
Domain Package
.
Public Tagged Services
Services can be wired together using tags. Global tags are provided by the Domain Package
by definition.
Every Public Tagged Service
must implement a tag related interface, provided by the Domain Package
.
Instance Handling
Every Package
has to provide a solution to access the Public Services
.
For example in a typical Symfony Application, a Package
could be represented as a Bundle.
Tips
Packages
do the hard work, don’t overengineer them. No more abstractions, keep performance in mind and do the job.
This doesn’t mean that every Package
should be a mess, it means, Packages
can be rewritten.
Packages
should focus on one requirement, this ensures that a Package
is easy enough to be rewritten.
Deleteability
DPS
enforce to write deleteable code. Small interfaces are representing all of
the functionality that has to be shared between Packages
and the Project Package
.
Packages must
be able to rewrite within 2 weeks by one developer.
DPS
ensures that parts of the application can be rewritten.
In agile teams this ensures, that one Package
can be rewritten within one sprint.
Faq
Where Should Repositories Live In A DPS
Structure?
DPS
don’t focus on the differences between repositories and services.
For example if you use doctrine, the repository is part of the implementation detail
of a doctrine specific package. The Project Package
never deals with any kind of repository.
In case of managing products, the Package
may
make use of an doctrine repository.
It’s up the the developer of the Package
, if the repository implements some kind of product service
interface provided by the Domain Package
or if the Package
uses one or many repositories under the hood.
<?php
//....
use [Project]\Domain\Service\ProductServiceInterface; // 1*
use Doctrine\ORM\EntityRepository;
class ProductRepository extends EntityRepository implements ProductServiceInterface {
/**
* @return ProductInterface|null
*/
public function getById($productId) { // 2*
return [
'products' => $this->find($productId)
];
}
}
OR
<?php
//....
use [Project]\Domain\Service\ProductServiceInterface;
use [Project]\DoctrineProductService\Dto\ProductDto;
use Doctrine\ORM\EntityRepository;
class ProductService implements ProductServiceInterface {
// ...
public function __constructor(EntityManager $em) {
$this->em = $em;
}
/**
* @return ProductInterface|null
*/
public function getById($productId) {
$products = $em->getRepository(ProductDto::class)
->find($productId)
;
return [
'products' => $products
];
}
}
(1*) It doesn’t matter what kind of class the ProductServiceInterface implements.
Other Packages
must not use the repository, just the ProductServiceInterface.
(2*) It’s totally fine to declare typically repository functions provided by EntityRepository in the ProductServiceInterface as long as they take some kind of structured data / object or scalar types as input. For example it’s a bad practice to share a generic findBy(array $args). Such methods should be replaced by special methods. This makes refactoring much easier and your public API stays as small, as possible (even with more methods).
Even if the second example isn’t that smart, it don’t violate DPS
.
Everything is hidden between the ProductInterface and can be rewritten without changing
any Project Package
or any other Package
.
Where should Entities / Dto’s live?
Entities and Data Transfer Objects (Dto’s) lives in the related Package
.
Consider there are two different implementations of a product Package
.
One implementations takes care of loading products from a MySQL database. Now the team wants to change the way how products are stored, the idea is to challenge PostgreSQL against MongoDB as product storage.
The team starts developing 2 new Packages
.
The structure now looks:
tree .
|-- MySqlProductBundle
|-- Dto
|-- ProductDto
|-- ProductVariantDto
`-- // ...
`-- PostgresProductBundle
|-- Dto
|-- ProductDto
|-- ProductVariantDto
`-- // ...
`-- MongoDBProductBundle
|-- Dto
|-- ProductDto
|-- ProductVariantDto
`-- // ...
Now the team can play around with all these solutions and pick the best one.
Packages
should be as smart as possible, developing such a Package
should
never takes more than one sprint (max. 14 days) for one developer.
A typical scrum team with 5 backend developers theoretically should be able to build
5 different implementations of the same Package
peer sprint.
All these Dto’s must implement the ProductInterface provided by the Domain Package
.
Deleteing / Replacing Packages
must
be easy.
In Symfony, Is A Package A Bundle?
well, it seems to be a good idea, to model the Project Package
as an AppBundle,
Packages
as Bundles and the Domain Package
as Library.
Why Is A Package Not A Library?
Packages
have to expose some kind of service configurations.
Libraries can’t integrate services without becoming a Bundle.
DPS
makes it easy to refactor Packages
to libraries, if (and only if) your requirements are changing.
In PHP, Should Every Package Live In A Composer Package?
In theory this works great, in practice, it’s much easier to develop the Packages
and the
Domain Package
next to your main Project Package
.
If and only if the Project has more than one Project Packages
it’s a good practice to use a
subtree splitter, to provide Packages
and the Domain Package
as composer package.
How To Model Deep Bidirectional Object Structures, Like Huge ORM Object Graphs?
DPS
enforce multiple small Packages
. Huge object graphs violates the idea of DPS
.
Most time it makes sense to define one Package
for one aggregate root (DDD).
An aggregate root must not
contain other aggregate roots.
How To Model References Between Object Structures?
References must
be modeled using an identifier. An identifier could
be any scalar value or
a dedicateded Id Object. The Id Object must
is part of the Domain Package
knowledge,
therefore it must
implement a dedicated Interface provided by the Domain Package
.
<?php
//....
use [Project]\Domain\Domain\ProductInterface;
class Product implements ProductInterface {
/** @return string */
public function getId() {
// ...
}
}
OR
<?php
//....
use [Project]\Domain\Domain\ProductInterface;
use [Project]\Domain\Domain\Product\ProductIdInterface;
use [Project]\ProductBundle\Dto\ProductId;
class Product implements ProductInterface {
/** @return ProductIdInterface */
public function getId() {
return new ProductId(/* ... */);
}
}
Why Enforce DPS
Small and Deleteable Packages
?
DPS
is about writing as simple and as clean code as possible.
To enrich this DPS
ensures that with changed requirements or new
technical solutions Packages
can
be rewritten.
DPS
is written with Facebooks The Hacker Way
in mind.
“Be Open” for other implementations.
Every Package
“can be replaced by “a better one”.
Make sure, that everyone can play / hack on new and hopefully better implementations.
“Focus On The Impact” of every Package
, “Move Fast” and “Don’t Be Afraid” by changing,
deleting code.
Every Package
should make strict decisions, this ensures that the Package
is simple and the implementation highly specific to the problem it solves.
The Domain Packages
Interfaces are responsible to make sure, that everything
is replaceable by a better implementation.
Packages
must not contain Abstractions
, Packages
must
implement
decisions. Decisions the Project Package
and the Domain Package
must not
take.
Why DPS
Makes Developers Happy?
DPS
focus on a clean and easy structure, that is really hack and replaceable.
As developer you can change and rewrite parts of the application fast, change
parts of the technology stack and it’s hard to encounter situations, where
the design of the software slows you down.
As developer you’re able to play with the best implementation for a Package.
Why DPS
Makes Product Owner Happy?
Packages are highly specific to a problem. Most time this means, the application
performs very well. During a longer project the complexity stays low, this
ensures that the development team isn’t slowed down. DPS
focus on a
clean structure, this ensures that it’s very hard to end up with a big ball of mud.
Why DPS
Makes Your Company Happy?
DPS
Focus on small, but nearly perfect Packages
. This means the development
team isn’t building one huge software.
For new team members it’s very easy to learn enough about the project,
to work on it. This ensures that new team members can join and unjoin the team efficiently.
Projects written using DPS
should
never need be rewritten.
DPS
preferes rewriting a Package
whenever there is a better solution,
over huge and risky rewrites of the complete software.