- Published on
Composing a PIE
- Authors
- Name
- Damian Płaza
- @raimeyuu
"Have you ordered a burger?"
Imagine that there's a person, who owns a foodtruck, and sells the best burgers in town.
The secret is how they prepare burgers and how they prepare sauce, invented by the founder.
The main purpose is to let the owner to grill the meat and keep the sauce secret always available.
To do so, the owner decided to hire a person that will be responsible for taking orders and deliverying burgers when they are ready.
Another role in the process is responsible for ensuring buns are ready, ensuring other ingredients are available (like lettuce, tomato, etc) and finally assembling the burger.

There are many interactions between each role to serve the customer.
A customer initiaties the dialog by ordering a burger (1), the person responsible for taking orders passes (2) the relevant details to the owner that knows all the secrets behind the best experience, which then passes (3) the work to the role that assemblies the final product. Then the product is passed (4) to the person responsible for taking orders, who then delivers (5) the burger to the customer.
The purpose is easily achieved - serving the best burger experience to the customer by organizing the collaboration in such a way that the owner focuses on the secret sauce and secret way of grilling the meat.
But change, change never changes...
"Too many orders!"
Too many orders are coming in, and the role responsible for assembling the final product is a bit overwhelmed.
All takes much more time, brings more cognitive load - customer experience is not as good as it used to be.
The owner is not happy about that which also impacts creating secret sauce and grilling the meat.
The decision is made - a former role responsible for both ensuring all other ingredients are available and assembling the final product is split into two roles.
This of course, impacts the whole process and the way the roles interact with each other.
This work is done in order to support the new purpose - serving more orders, decreasing the cognitive load and making the owner happy again.

Now, after the way of organizing the work is changed, goals were easily achieved!
Initiating (1) and finalizing (5) interactions remained stable and unchanged - but the customer is not aware of that - which of course is a good thing.
Contrary to that, the role responsible for taking orders passes the details (2) in parallel to the owner and to the role responsible for ensuring all ingredients are available. When both roles finish their work, they pass (3) the results to the role responsible for assembling the final product. Then the final product - burger - is passed (4) to the person responsible for communication with the customer.
Concluding, roles and interactions got organized to support purposes defined by the owner of the foodtruck.
"New" (communication) structure emerged as there were new conditions that the foodtruck crew started facing.
"Have you ordered a processing?"
Now, imagine that we are doing data processing by using a few, distributed services.
The organization of work got hidden behind the API Gateway in order to have ability to change how the results are produced.
Ah, finally microservices! (please tell me you know I am joking, dear Reader!)
They have a bunch of interactions (2, 3, 4) between each other - each service is responsible for a specific part of the process. The communication (1) is initiated by a consumer with the API Gateway and the result is delivered (5) through it too.

Service B interacts with service C in a sequential manner, as the service C requires the byproduct of service B to do its work.
But change, change never changes...
"Too many responsibilities!"
Service C is quite overwhelmed with the amount of work it has to do.
CPU, RAM and other resources are not enough to handle the load.
But the biggest problem is that the services C changes too often, which impacts the whole process.
There are multiple reasons for it to change, which makes the whole value delivery chain a bit fragile.
A new goal emerged - we need to reduce the cognitive load of service C, so it can focus on its main purpose and be more stable.
This of course makes maintenance easier too.
The decision is made - a part of responsibilities of service C will be moved to a new service D - which will be responsible for a specific part of the process.

Introduction of service D changes the process but the consumer is not aware of that. Initiating (1) and finalizing (5) interactions remain stable and unchanged.
Then service A dispatches (2) the work to services B and services C. After they both finish their work, they pass (3) the results to service D, which then passes back (4) the final result to the service A.
As new expectations emerged, the way we organized interactions between services changed to support them.
Great that we designed at least some boundaries!
"Have you ordered a computation?"
Let's take another example, this time with object-oriented design.
There is an interface that a consumer uses to interact with a module. There's an object A that implements one of the capabilities promised by "the interface". This capability has a specific contract that the object A has to fulfill in order to accept the request (1) and provide the result (5).
Internally, object A interacts (2) with object B, which then interacts (3) with object C to do its work. When object C finishes its work, it passes (4) the result back to object B, which then passes it back to object A.

Everything works great so far.
The purpose is easily achieved - the consumer gets what was promised by the capability provided by the object A.
But change, change never changes...
"Too many responsibilities!"
Object C is quite overwhelmed with the amount of work it has to do.
Turns out that the it's quite hard and complex to understand the details of the object C, which makes the maintenance a little bit harder.
Also, this causes tests to be hard to write, hard to read and hard to evolve.
A new goal emerged - we need to reduce "the complexity" of object C, so it can focus on its main purpose and be more stable.
This will reduce the "cognitive load" of designers, maintainers and engineers too.
The decision is made - a part of responsibilities of object C will be moved to a new object D - which will be responsible for a specific part of the process.

Now, object A dispatches (2) the work to objects B and C. After they both finish their work, they pass (3) the results to object D, which then passes back (4) the final result to the object A.
The contract remained the same and all consumers didn't see a difference.
Good that we had boundaries!
A pattern?
You might be wondering, dear Reader, what the hell I want to express through those examples? (I really hope you do wonder, dear Reader!)
Turns out that there is a common plane for all of those tiny scenarios - even though it might not be that obvious at first glance.
In each of them, we experienced initial purpose(s) and evolution through time.
The evolution made the organization (of work, of interactions) to be changed to support those new goals.
This was the driving force that required the structure (of communication) to be adjusted.
Also, without boundaries, it would be enormously difficult to do any re-organization as consumers/customers would be affected by the change.
Of course, this isn't always possible to not break the contract, but as we saw, in those examples, we were able to observe so.
So a bunch of "elements" (roles, services, objects) interacted with each other to achieve a purpose "greater" than themvelves.
Then, a new, desired capability emerged, deliverying the value, defined by the goal.
PIE?
One could say that everything starts with a purpose.
It defines the scope, the boundary in which we are going to operate, trying to find the way of acheving it (the "how").
When it is specified, we can think of the interactions that appear to be on the boundaries of "an organization".
And let "an organization" be a set of roles that human play, a set of services or a web of objects - "it does not matter".
"It does not matter" means that a context exists in which all of them might be thought of as "same".
One can easily raise the level of abstraction and think of roles/services/objects as elements.
"A piece of reductionist", you might say, dear Reader.
Yes, indeed.
But at the same time, "all models are wrong (=imperfect), but some are useful", as the great saying from George Box goes.
This gives us certain model to use while thinking!
Then, composing interactions between elements enables the expected behavior (the expected capability) to emerge to achieve the purpose.
Purpose, Interactions, Elements.
PIE.
Composing a PIE
There are recurring tides when people bash object-oriented programming that is ineffective, full of boilerplate and leads to a lot of complexity.
Which might be true, when one focuses on the structure, on the elements themselves.
As we already discussed in The ambiguity of objects, there might be a great misunderstanding of the tools we are using.
This leads to brittleness, complexity and other problems.
In fact, we could think of our job to design interactions first.
Interactions driven by the purpose we want to achieve (you might be interested in The ambiguity of software architecture post too).
Eventually, "elements" (also known as components, modules, etc.) will be required to deliver needed capabilities to achieve the purpose.
But it all starts with the purpose.
A system's capability to fulfill its Purpose emerges from Interactions between Elements.
Even though we are abstracting away the details, we use reductionist approach, it might still be useful.
As we saw, thinking in PIE terms appears on various levels: organizing the work of human beings, organizing the communication between services or designing how objects collaborate with each other.
One could easily point at one of the greatest books on software design (in my humble opinion), Balancing Coupling by Vlad Khononov where a concept of "Fractal Design" is mentioned.
This is exactly what I am trying to express here - the same principles apply on different levels of abstraction.
And by incorporating "PIE" thinking, we can consciously design our systems - not focusing too much on the structure (for sure not at the beginning of the designing process!).
There is a great maxim, which originated in architecture (not software!) and industrial design, that goes like this:
Form follows function.
Which means that the structure of a system should follow the purpose it is designed for.
The communication structure (form) between elements should follow the purpose (function) of the system.
As designers, we don't know what elements will play required roles in the system, but we must understand the purpose and interactions that will be required to achieve it.
When I say "interactions", it might mean "capabilities provided by certain roles".
Those capabilities might be located in a monolith, in a set of distributed services - it is not specified at this point.
How we would organize it all, vastly depends on a variety of forces, appearing in the context we are operating in.
"Ah, you like talking about abstract stuff Damian, ain't you?", you might say, dear Reader.
But it all boils down to focusing on the behavior and specifying it firstly, from he Outside-In (consumer) perspective.
We can specify the behavior through Event Storming notation, through Open API Specification or through tests (you might find The ambiguity of TDD interesting).
By looking from the usage, from the consumer's perspective, we can define specify contracts ("shape") and protocols (the way of interacting with it).
So depending on the abstraction level we are designing, it can be an object's method, an actors' message handler, a service's entrypoint (e.g. HTTP endpoint), etc.
But what is it all about?
Change, change never changes
Do you want to be a great architect?
Think about the purpose first - "what is the main problem we want to solve right now?".
Knowing what, you can think of how to achieve it.
What capabilities would need to be there to achieve the desired goal?
Also, it's not a coincidence that Purpose in "PIE model" is the first letter.
Its job is to remind us all the time that the purpose (answers to what? and why?) must be clearly defined, or at least understood, before we start jumping into the solution (answers to how?).
Then we have Interaction on the second place, which should remind us that the communication structure (protocols) between elements always follow the purpose.
Finally, we have Elements, which will take part in the interactions and provide required capabilities.
Those elements might be human beings, services, actors, objects, enterprises, teams, and so on.
As you might probably noticed, dear Reader, one inevitable fact that appeared in each example was the change.
The change that was triggered by the redefinition of the purpose or totally new purposes appearing.
To be able to respond to a change (responding to a change? You might be interested in (Fr)Agile), we need to have boundaries.
Boundaries that encapsulate the details of interactions between elements so that we can reshape the structure of communication without breaking the contract (at least trying to avoid it).
Thanks to boundaries, we can evolve our systems - whether they are collaborating humans, interacting distributed services or actors/objects sending messages to each other.
Next time dear Reader, when you will be designing a part of a system, think about the purpose first and remember that:
What you hide, you can change.