Code architecture: Overview of Supply team technical challenges

Jponzvan
Mercadona Tech
Published in
8 min readAug 31, 2022

Hello everyone, in this article, I will give my opinion (totally subjective) about the importance of architecture to achieve symmetry in projects, facilitate new team members’ entry, and improve communication within the team itself. 🙂

My name is Juanjo, and I am currently a backend developer in the Supply team. The Supply team oversees the provision of hives, stock management and control, and allows us to maintain key efficiency levels which help to accurately manage warehouses or logistics. We are in charge of sending the supply order every night to prepare the orders. In parallel, we are developing an application to help the stores make their processes more manageable. We are making this application by consuming existing services.

What architecture do we keep up in the supply team?

We are currently developing using `interaction-driven design,` so our architecture is very similar to hexagonal architecture. Why do I say very similar? Because the reality is that we aren’t yet using all the potential it brings, but we are at a point where it is evident what our business logic is and where we let Django and Django-rest-framework, our framework used to build the APIs, work on.

In addition to the hexagonal architecture, it would be very unfair not to say that we are `event-driven` to decouple at the points that we need to and to be able to develop focused on the problem that concerns us at all times. (All is developed under IDD ).

We develop using TDD, but for simplicity of the article, we skip the whole part of the tests.

Taking advantage of the architecture topic, I will name the basic design patterns we use daily as developers.
We try to achieve something very similar to:

Image from https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html (Robert C. Martin (Uncle Bob))

It isn’t necessary a complete knowledge about hexagonal architecture, but it is recommended to be able to keep up the thread of the article without any problems!

I wouldn’t say I like articles with a lot of theory and no code, so I will try to write as many examples as possible to clarify mixing with the theory 🙂 . Feedback is always welcome, so feel free to comment if this way the text has become cumbersome 😀

Hands on with an example

If we develop with Django, then… how do we do everything? Let’s take a look at the flow of an API call 🙂

It all starts with an API REST call to a random endpoint; we don’t keep up a CQRS pattern, so regardless of what our endpoint does, it will follow the same way of working.

Let’s imagine the `SupplyOrder` entity (as we have already said that we use Django as a framework, let’s show a real example of how we work with it), which is defined as:

class SupplyOrder:
supply_line = ForeignKey

win an endpoint:

  • POST http://base.com/supply_order/add_product by means of which we will add lines to our supply order:
{
"product_identification": "11111",
"quantity": 15
}

As we have already said, let’s see how we would create the endpoint by keeping up with the architecture and structure of our project.

Add products to our order

As we have already said, we have a POST to `http://base.com/supply_order/add_product` with the payload

{
"product_identification": "11111",
"quantity": 15
}

View as Infrastructure

This is the typical view we use in our project (django-rest-framework actually):

class AddProductToSupplyOrderView(APIView):
def post(self, request, *args, **kwargs):
# We adapt the answer to a dictionary that knows our domain and we can
# we take advantage of the power of django-rest-framework serializers to
# validate the type of data we receive. This is the adapter pattern
added_product = AddProductSerializer(request.data)
added_product.is_valid(raise_exception=True)

# Once the data has been validated, we open the door to our domain to them.
# a través de las acciones, las cuales siguen el patrón command handler
AddProductToSupplyOrder().execute(added_product.data)
return Response()

What are we achieving with this? All our views look the same, so everyone in the team is very clear about how we should develop, and we have no doubts about where the business logic has to go — in serializers, in actions, or in the view itself?

Why do we use dictionaries instead of a value object to model the added_product?

We care about infrastructure and orchestration. In an ideal world, or if we were to follow 100% of the programming practices indicated by IDD (or even just those indicated in the famous clean code book), the serializer should return an immutable value object that references our domain (in this case a value object called ProductToAdd for example). Why don’t we do it then? Because within the team, we have decided to be a little more flexible in this kind of things (always by consensus), and we decided to create less boilerplate to make the day-to-day programming lighter. Currently, this practice is working for us and not giving us problems. However, a book adapter pattern like the one we use in the serializer should return “something other than a dictionary.” Still, the article tries to be totally faithful to reality 🙂.

Actions, our application layer

Ok, we have named the actions, but what are these really? As I have already mentioned, we develop trying to use IDD (thanks, Sandro Mancuso), so for us, actions are all the interactions that occur with our system. In an understandable way, and eliminating complexities such as asynchrony, actions are all the possible interactions that a user has with our system. This is known as the application layer.

And what do actions do? Actions are in charge of orchestrating all the calls between objects in our domain so that the desired interaction can be performed. In this case, adding a product to a supply order.

class AddProductToSupplyOrder:
def execute(self, product_data):
supply_order = SupplyOrder.objects.last()

# We have a strong object-orientation, so supply_order model
# is in charge to add the product
line = supply_order.add_product(
product_identifier=product_data['product_identifier'],
quantity=product_data['quantity']
)
line.save()
supply_order.save()

As before, and because we haven’t needed it yet (at least in supply), here we see friction with what would be a textbook DDD.

We access the database directly without using a repository.

We use the same database model to “fill” it with methods from our business logic.

Again, why do we do this? For simplicity. At some points, we are considering using the repository pattern and injecting it into the actions as the canons dictate, but we haven’t needed to do this yet; the tests are fast, and it allows us to develop using less boilerplate. In addition, the entry of people into the team is much smoother because they should not have extensive knowledge of DDD or hexagonal architecture, they are gaining it little by little, and we have identified the technical debt that we leave along the way.

We do keep up some IDD/DDD rules that we have kept:

  • an aggregate (in this case supply_order ) could never modify other aggregates. With this statement, we see that line is an entity (not an aggregate) of supply_order, and it is the aggregate that creates these entities. This refers to the different types of entities that DDD has: aggregates, entities, and values objects.
  • the saving/retrieval of information is done in the actions and not in the models. This means that supply_order should not use anything from products if it has not been previously preloaded. It is because of this that the line.save()is done in the action as if we had a repository and the supply_order method returns the line created. We do not let the models access the database.

Domain and its importance

We have already seen that the action is the entry to our domain, but what exactly is the domain?

The domain is all those “actions” that our application layer performs (which in this case and by coincidence are also called actions) on our aggregates.

  • Modify/Read a state
  • Query certain information from an external source
  • Validate data consistency

In this state, it is crucial to know that in our team, we try to take object orientation as far as we can, so we apply techniques such as tell don’t ask, or rules that the modifications of a data (or state) of the model must always be done by itself, and not by the action itself, that is why we have the add_product method. We do not create the object directly from the action.

Bonus track: Dependency injection in our actions

First of all, what do we understand that we should inject into our actions?

All those external elements interact with our system in some way.

We have already seen that we don’t use the repository pattern in our actions, so do we use dependency injection? Not as such. Is it because we don’t believe in it? Not at all; it is because we have not yet found any framework that makes us fit completely or a problem that we have been unable to solve by doing a pseudo injection to be clear that it is an external element of our system. So, what would this look like? Let’s imagine that we need an action to communicate with an external API. We would model this as follows:

class ActionMolona:
def __init__(self):
self._client = ExternalClientImplementation()
def execute(self):
self._client.some_cool_method()

What does this allow us to do? To have perfectly tracked the elements we know we want to inject, and when would we do that? We have not yet reached the point where we need to change an external dependency because the contract has been broken. Still, we are a very open team, so at the moment, we would have to shift ExternalClientImplementation to NewExternalClientImplementation at all points. I am convinced that we would start injecting it. And why at that point? We like to think that we program for today, not thinking in the future, and with this, we have tracked the friction points that we may want to inject in the future, and when testing, not using a dependency injection framework, it is much simpler and faster.

We don’t see a problem in leaving debt controlled technique, as long as we know what we are doing 🙂

Project file structure

The importance of the architecture, in addition to the symmetry of the code in the project, is the facility that it offers at the time of knowing where we must place the files, a doubt that everybody assaults us on more than one occasion when we do not have the necessary symmetry.

In this way and with the example described above, the structure would be as follows:

.
└── supply_order/
└── src/
├── views.py
├── actions.py
├── models.py
├── serializers.py
└── clients.py

By consensus in the team, it has been decided that as long as it is maintainable by the number of lines, all actions will go inside actions.py, as well as serializers in serializers.pyfiles and external clients in clients.py.

If another type of infrastructure is born (applying the repository pattern, for example), we would create a new infrastructure.py file. Still, for the moment, with this type of architecture and file arrangement, we have achieved the goal we want.

Conclusions

Infrastructure is not only essential when developing sustainable code. It’s one of its great virtues, but a good infrastructure makes it easier for new people to join the team since, from the first point, it is apparent where to attack and how to do it.

In the same way, when extending the code given a feature, it is very easy to go directly to the action you have to modify/extend instead of going crazy looking at which part of the code you have to update. 🙂

We need you!!

We are always hiring! We are looking forward to starting playing with new concepts and having new teammates, so feel free to apply if you like the way we work 😀😀

--

--