Applied Patterns & Practices From The Trenches
Thomas Ploch - Principal Software Engineer @ Flix
2015 merger with competitor Flixbus to form the largest long-distance bus travel provider in the German market
Mid-2017 our modernisation journey started
Development began 2012 with a single development team with support from one near-shoring team in the Ukraine
Tech Stack
Image: Cover "No Silver Bullet—Essence and Accident in Software Engineering". IEEE Computer (April 1987).
Image: Cover "No Silver Bullet—Essence and Accident in Software Engineering". IEEE Computer (April 1987).
Somehow we, as an industry, still consistently end up, repeatedly, with many silver bullets.
And it was not different with us!
This architecture pattern was the most-widely used software & framework architecture for web-based projects in 2012.
What is ...?
In our case the cycle time kept increasing!
At some point we couldn't even implement certain features - "that's not possible..."
class OrderEntity {
private DateTime $createdAt;
private string $status;
private MutableCollection $items;
public function getStatus(): string
{
return $this->status;
}
public function setStatus(string $status): void
{
$this->status = $status;
}
public function getItems(): MutableCollection
{
return $this->items;
}
}
// Business logic is spread out to many services
class OrderService {
public function cancelOrder(int $id): void
{
$order = $this->fetchOrderFromDB($id);
$status = $order->getStatus();
// some validation & processing logic
$order->setStatus('cancelled');
$this->saveOrderToDB($order);
}
}
When the classes that describe the model and the classes that perform operations on the model are separate. The services contain all the domain logic while the the domain objects themselves contain practically none.
The classic Lasagna architecture
Blood, Sweat & Tears
Time
Anemic Model
Rich Model
The really sweet spot
Time to complete
The sweet spot
Time to complete
Time to complete
The horrible spot
Use Case A
Use Case B
Use Case C
Use Case D
Order
Service
Initially you start with a single use case and everything seems perfectly fine
Now the services are starting to be shared between differing use cases
More and more use cases make use of the shared service because DRY, right?
And over the time this initially nicely fitting Order Service has become an unmaintainable mess suffering from the God Class symptom
Order
Entity
Anemic Model
Order
Service
Order Repository
Order Item Repository
Use Case A
Use Case B
Keeping consistency is IMPOSSIBLE!
Use Case A works on Orders and let's the Order model handle the internal consistency.
Use Case B breaks the consistency boundary by operating on the collection of Order Items directly.
Order
Item
Order
Item
Order
Developers will very often take the path that was paved before they joined
Developer is fed up with the messy system and leaves the company
New developer is hired
New developer follows the paved path within the existing system
Problems get worse and new developer struggles with delivery
Developer wants to change things but stakeholders do not see the necessity
Service B
Service C
Service D
Service E
Service A
Use Case A
State
State
State
State
State
Lasagna architecture made it very complicated to add and change features
State management was spread across many services and a major source for bugs and incidents
Developers were very unhappy and the re-hire cycle even accelerated the problems
All problems reinforced themselves and spun out of control very quickly
New path
Identify the most valuable components and their boundaries
Align the organisation around the future boundary cut-marks
Measure & analyze the current system's migration complexity
Pick the next most valuable component and start the migration
Finish the migration and go back to step 4
Identify the most valuable components and their boundaries
Align the organisation around the future boundary cut-marks
Measure & analyze the current system's migration complexity
Pick the next most valuable component and start the migration
Finish the migration and go back to step 4
The Map places the Value Chain components on an economic evolution horizon. The more commoditized a component is, the less there is a need to build & maintain components yourself.
Purpose & Scope set the stage for the mapping exercise
Anchoring the Value Chain on the users' needs is important to keep the focus on the customer value.
The Value Chain identifies
dependencies in the value delivery. This helps with understanding your actual core capabilities.
You want everything in core to take priority because you believe that this will give you an edge over your competitors.
Becoming faster in delivering value has the highest ROI in the core quadrant.
Identify the most valuable components and their boundaries
Align the organisation around the future boundary cut-marks
Measure & analyze the current system's migration complexity
Pick the next most valuable component and start the migration
Finish the migration and go back to step 4
...if you have four groups working on a compiler, you’ll get a four-pass compiler.
Melvin Conway
.git CODEOWNERS
Team B
Team A
Module A
Module B
Identify the most valuable components and their boundaries
Align the organisation around the future boundary cut-marks
Measure & analyze the current system's migration complexity
Pick the next most valuable component and start the migration
Finish the migration and go back to step 4
Static
Behavioral
Risk
A
B
C
D
This will probably never be migrated, and may even be a trigger to give up on this component completely!
Identify the most valuable components and their boundaries
Align the organisation around the future boundary cut-marks
Measure & analyze the current system's migration complexity
Pick the next most valuable component and start the migration
Finish the migration and go back to step 4
SELECT * FROM orders where...
INSERT INTO orders VALUES...
UPDATE orders SET ... WHERE
Use Case C
Use Case D
Use Case E
Use Case F
Use Case B
Use Case A
Traces connect the database and use cases through the code
Tip: Start with the INSERT use cases first, since they represent the genesis cases.
Most tracing tools/agents support the major database clients and connect queries with code traces and/or logs.
Span
Span
Span
Span
Span
Span
OpenTelemetry / observability platform
class Order {
private DateTime $date;
private string $status;
// Impossible to construct an invalid state
public function __construct() {
$this->date = new DateTime(
'now',
new DateTimeZone('UTC'),
);
$this->status = 'new';
}
}
class Order {
// ...
public function cancel(): void {
// now the validation is within the model
if ($this->status === 'cancelled') {
throw new LogicException(
'Invalid status cancelled'
);
}
$this->status = 'cancelled';
}
}
class OrderService {
private OrderRepository $repository;
public function cancel(int $id): void
{
$order = $this->repository->get($id);
$order->cancel();
$this->repository->save($order);
}
}
Use Case A
// The Strangler Facade is just an abstraction
// It will have multiple implementations
interface OrderFacade {
public function place(
Id $id,
Money $value,
CustomerId $customerId,
): OrderCreatedEvent;
public function cancel(Id $id): OrderCancelledEvent;
}
Strangler Facade
Transactional Outbox
TX boundary
Order
Event
New
Order
Service
Outbox
Consumer
This has to be repeated for all the identified use cases
Once we have covered all the use cases with the Strangler Facade the new service is completely in sync with the legacy system!
Strangler Facade
Remote Client
Order
Event
New
Order
Service
Legacy Sync Consumer
All the changes behind the Strangler Facade are completely transparent to the clients!
Keeping the legacy system up to date for some time will be necessary due to reporting and other use cases!
Finish the migration and go back to step 4
Start with creating a new target picture based on strategic and economic factors. For us Wardley Maps, Strategic DDD and Team Topologies worked.
Realign the organisation around the new system architecture. Inverse-Conway Maneuvers worked for us, but it's not for everyone since it is a huge social perturbation.
Measure & analyze the code, the behaviors and the risks before diving into implementation. You'll often find surprises!
Design your new APIs using collaborative modelling and modern approaches. In the end, that's what modernization is all about.
Leverage observability platforms and other tooling to help you find the entry points to the use cases you want to tackle - by analyzing the traces from the DB to the code entry-points.
Tackle each of the entry-points with the newly designed Strangler Facade. We used the Transactional Outbox pattern and asynchronous event integration successfully.
Continue while it's valuable for the organisation, but constantly inspect and adapt.
Slides online