Separating your bounded contexts
A challenge when implementing bounded contexts is managing the dependencies between them. We always aim to make bounded contexts as independent as possible, but making them completely independent is impossible and unwanted. Software tends to be useful only when the different parts talk to each other.
The ideal way to make bounded contexts communicate with each other is through emitting events. One bounded context emits an event and other bounded contexts can listen to that event and act accordingly. This works especially well if an action needs to be triggered in another bounded context.
Let’s take an example from a conference organizing software. When adding another room to the venue, we want to increase the amount of tickets we sell. Luckily our software automates this process for us.
When we add the room in our Planning context, a RoomAdded
event will be emitted, containing all relevant data like the capacity of the room. Our TicketSales context can listen to this event and increase the amount of tickets available, send an email to people on the waiting list, et cetera.
But sometimes, using events feels like overkill. Sometimes, a bounded context just needs some data from another bounded context.
For example, the TicketSales context wants to show details for a workshop on a designated page so that the customer can make a more informed choice. Workshops, however, are a concept from the Planning context. Using events, the Planning context would need to emit a WorkshopPlanned
event and probably many others. The TicketSales context can listen to those events and maintain its own list of workshops, just for displaying purposes.
You may feel that this is really over-engineering and it would be hard to disagree. Following this pattern makes sense when working in a microservices architecture, but makes less sense when working in (or towards) a modular monolith architecture.
Using the facade pattern
A less complex approach could be to simply query the existing database and get the workshop details. However, this breaks some rules we established for implementing bounded contexts.
Each bounded context has its own database schema and can’t access that of other bounded contexts. We don’t want a bounded context to integrate directly with the code in another bounded context. So how can we get a list of workshops then? The facade pattern can help us solve this problem.
First of all, when talking about facades, I’m not talking about the Laravel facades. I’m talking about the facade design pattern. We will build a facade around each bounded context.
This facade will expose methods other bounded contexts can use to query data. This facade should be treated as an API we expose to a third party. It should hide internal implementations and it should aim to not have breaking changes.
Show us the code!!1!
So how does this work in practice? We start with the functionality required in the TicketSales context. This context needs to be able to query for Workshops, so we create a Workshop class and a WorkshopRepository class to implement this functionality. Since we use Hexagonal Architecture in our TicketSales context, we create the repository as an interface.
Workshop.php:
<?php
namespace App\Ticket\Sales\Domain;
class Workshop
{
public function __construct(private string $id, private string $name, private string $description, private string $tutor){}
}
WorkshopRepository.php
<?php
namespace App\Ticket\Sales\Domain;
interface WorkshopRepository
{
/**
* @throws WorkshopDoesNotExistException
*/
public function findById(string $workshopId): Workshop;
}
I removed most methods from the Workshop
class to save space, so the example class only contains a constructor. In real life, this class contains very specific behavior to support the functionality we have in the TicketSales context.
The WorkshopRepository only needs a findById
method, because we don’t require anything else right now.
Now, let’s go to the Planning context for a bit and see what’s available there. The part most relevant for us is the facade and what functionality it offers that we can use. Luckily, there already is a findWorkshopById
method we can use.
PlanningFacade.php
<?php
namespace App\Planning;
use App\Planning\Models\Workshops\Workshop;
class PlanningFacade
{
public function findWorkshopById(string $workshopId): ?Workshop
{
return Workshop::find($workshopId);
}
}
There are probably many more methods in this facade, but we leave them out to save space on this page and in our brains.
We can see some interesting things here. First of all, this facade is located pretty high up in the namespace tree. The facade is really a representation of the entire bounded context and its functionality, so having it close to the root namespace makes sense to me. If the facade grows, it may make sense to split it up into smaller facades and organize them using namespaces.
The attentive reader will now notice that this would go against the guideline to avoid making breaking changes to the facade. However, we’re working in the same repository and automatic refactoring would go a very long way, if not all the way, in fixing the effect of a breaking API change.
Furthermore, we define a findWorkshopById
method. It accepts an integer value as workshopId
and returns a Workshop or null
. This is now the internally exposed API of this bounded context.
We expose certain concepts to the outside world, but we also hide certain implementations. We expose the Workshop
model, but we hide the implementation of how we find one. Do we use a repository or do we use the eloquent methods directly?
These implementation details are not exposed and could be changed without breaking the API. We could switch to using Doctrine in this bounded context and the API would not have to break. We would only have to re-implement this method in the facade. This is very important because it means we can evolve each bounded context without having to change things in other, dependent, bounded contexts.
If we are unsure about how stable the Workshop
class will be in the future, we could just as well serialize the class, but that would probably be overkill in this case.
Exposing the internal classes creates coupling between the bounded contexts, but they’re usually a bit easier to work with. When the class changes, it may affect multiple places outside the bounded context. This is a decision you need to make as a team, so be aware of the trade-off here.
All that’s left for us now is to glue things together. We will implement the WorkshopRepository
interface we created earlier and the implementation will call the facade.
WorkshopRepositoryCallingPlanningFacade.php
<?php
namespace App\TicketSales\Infrastructure;
use App\Planning\PlanningFacade;
use App\TicketSales\Domain\Workshop;
use App\TicketSales\Domain\WorkshopDoesNotExistException;
class WorkshopRepositoryCallingPlanningFacade implements WorkshopRepository
{
public function __construct(private PlanningFacade $planningFacade){}
/**
* @throws WorkshopDoesNotExistException
*/
public function findById(string $workshopId): Workshop
{
$planningWorkshop = $this->planningFacade->findWorkshopById($workshopId);
if ($planningWorkshop === null) {
throw new WorkshopDoesNotExistException;
}
return $this->createWorkshopFromPlanningWorkshop($planningWorkshop);
}
}
Let’s go over this class and see what it does. It depends on the PlanningFacade
, which is injected through the constructor. Because we use hexagonal architecture and we defined our interface before implementing it, we know this dependency will not leak into the rest of the bounded context code.
The implementation of this method is mostly translating concepts from the Planning context to concepts in the TicketSales context. We fetch the planningWorkshop.
If the Planning facade returns null, we throw an exception that the TicketSales context understands. We turn the planningWorkshop into an object that our TicketSales context can understand. I have hidden the implementation of createWorkshopFromPlanningWorkshop
to save some lines.
Using this approach, we only have a very small set of classes which must know how to talk to an external context. Using hexagonal architecture, we can keep this nicely separated from the code which actually solves the business problems.
Because we only talk to the facade, we also know we don’t have to care about inner implementations of the planning context. We can keep all the mess from that context out of our TicketSales context, and we only need to make the translation step in our implementation of the WorkshopRepository interface.
If we decide to make architectural changes to the planning context, our TicketSales context doesn’t care, as long as we don’t break the methods the facade exposes. And this is how to effectively use facades and hexagonal architecture to separate bounded contexts.
Member discussion