DEV Community

Krste Šižgorić
Krste Šižgorić

Posted on • Originally published at krste-sizgoric.Medium on

Software Architecture Should Not Be the Enemy

Software architecture is more than folder structure.


Photo by Hermes Rivera on Unsplash

Wikipedia says it is a set of structures needed to reason about a software system and the discipline of creating such structures and systems. So, it is a pattern that we follow to make it easier for us to comprehend how the system works.

Often it is not just one single pattern, but a set of architectural patterns that work well together. Each of those patterns brings some benefits and some restrictions. Each pattern is also a solution for a particular problem. Once we are aware of that, we can utilize the full potential that it brings.

If we do not have a problem that some pattern solves, then we definitely should not use it. Purpose of the solution is not to demonstrate that we know all the fancy patterns, but to solve real world problems. On the other hand, the purpose of the pattern is to solve a recurring problem in a safe and maintainable way. Additional benefit of a well established pattern is that it can be recognized by other developers, which makes it easier to understand the code and its purpose.

And best of all, if we understand the pattern that is used, we should be aware that breaking it when it is needed is OK. We should be the masters of our architecture, not the other way around. But in that case, when we break consistency, we need to have a really good reason for it. That reason should be commented on, and well visible in our code. Because the first question that comes to mind is “Why? Why are you doing this?”, and we should have an answer ready, written in the comments of the code.

Separating reads from writes

There are two types of actions that we can do with data. Either we are reading it (query), or we are altering it (command/mutation). We can mix them together, but there is a great benefit of keeping them separated.

By ensuring that retrieving data will never alter it, we can have confidence of fetching data wherever and whenever we want, and as many times as we want. There will never be unpleasant surprises of unconsciously making change on it.

When writes do not depend on reads, we can handle them differently. If needed, we can alter one side without forcing change on the other.

In case of update action on our REST API, if we are following RESTful guidelines, we should return a new state of resource. But in practice we often do not need it. Common practice on frontend after update is to display a success message and redirect to the index page. In this case our response is not needed. All we did is increase traffic between API and client.

The client could use cache, and by returning a new state it could update that value in the cache. But if response is not needed right away, we could just invalidate cache and new state could be fetched when and if needed, by simply doing that additional API request.

In that case we can do some magic on the backend. If our system ever grows big and we start having problems with performance due to a lot of data, we can use a different structure or source of data for the read side.

We can use a materialized view pattern and prefill a different table with data that is needed for querying and avoid the need for joining multiple tables. This makes the query execute much faster. We can split our read and write side in two different services, and scale them independently. We can do whatever we want, as long as we decide not to return a new state of resource on data mutation.

Yes, this can be done even if we do not split read and writes, but in that case every change on the read side will ultimately change the write side. If a model that needs to be returned changes, we need to change it in all places, including commands/mutations.

If we use different tables for data that are served for UI, we will need to generate new value directly from the application, immediately after create/update, so we can return it. This makes our application even slower, and it defeats the whole purpose of optimizing the read side.

Now that we are aware of this we can decide if this additional complexity is necessary. Do we optimize on frontend and return a new state right away? Or do we sacrifice that optimization on frontend to have benefits of separate reads from writes, and count that eventual consistency will be fast enough to meet our needs? It is up to us to decide what fits for each specific case.

Distinguish business logic by its purpose

Shortest way from point A to point B is a straight line. Equivalent of this in programming is procedural code. It is easy and straightforward. But that does not mean that it is the smartest choice. Crossing the crossroad diagonally is not something that we should practice on regular bases, even though it is the shortest way. It might be OK for small towns, where traffic is one car per hour, but in big cities you would be run over on the first try.

Same goes for procedural programming. It seems like it is the best solution until functionalities start to change and grow. In that case procedural methods tend to grow too. First solution is to split them into multiple methods, but code is still procedural.

Since we are already talking about update action on REST API, let’s continue with that. It does not matter if we are writing our logic in controller, service or handler, the end result is often the same. Code is written in a single method. Once additional functionality is needed, we simply extend that method. In case when it grows too big, we might split it into multiple methods that are executed one after another. But it is still pretty much procedural.

We could look at the logic of the method from a different perspective. Validation probably is not part of this method. It is done in middleware and, if the request is not valid, our method won’t even start to execute. Code responsible for fetching data is abstracted in a repository or some similar pattern.

What is left is business logic, which is something that is the main purpose of the system we are building. But not all code written in our method is the same. If we take a look of its purpose, we can see three different kinds of logic:

  • Main business logic — purpose of method
  • Critical business rules — rules that define our process and need to be applied every time when action is performed
  • Side effects — additional logic that needs to be executed in case particular event happened during method execution

We tend to mash them all together and keep them in the same place, but this does not need to be the case. Our method could only contain its main purpose, something that can be expressed with the name of the method. In case we have a blog, this would be publishing the article.

Critical business rules are something that distinguish our system from others, and represent our process. For blogs, a critical business rule would be that attachments of published articles need to be automatically set to public. This kind of logic is something that needs to be executed every time without exception. And it does not depend on anything except data inside entities, and their interaction. So, it is best kept within the entity itself.

Instead of setting the value of IsPublic field on our article from false to true, we could implement the method Publish() that will switch state to true, go through post’s attachments and make them all public. This way it is not possible to forget to do this critical business rule. To ensure that the IsPublic field is changed only through this method we could restrict it so it can be changed only from within the entity itself. We are now forced to call the method Publish() if we want to publish a post. Therefore, we are forced to apply the business rule every time. If the business rule changes, we can change it without affecting the main method.

But there might be some additional logic that needs to be executed on post publishing. For example, an author entity might have a counter of published posts. This should be incremented once a post is published. If we have subscribers, we want them to receive mail that a new post is published, too.

We can implement this in the same method, just after we publish a post. But we can decouple this logic by emitting a domain event that post is published. Then we can create handlers that will process that logic: one for author update, and other for sending email.

We can have as many handlers as needed. Additional functionality on post publishing is now just a new implementation of event handler. Main method is untouched. Other side effects are untouched. We cannot introduce bugs in existing code because we did not change it. The only things that need to be changed are integration tests, to check for new functionality.

Being aware that this approach exists, we can now determine if we need it. Do benefits outweigh complexity that is introduced? Is this the right fit for the system we are building?

Business logic in a controller

In today’s development placing business logic inside controllers is a big no-no. It has basically become a default way of implementing APIs in languages like Java and C#. We are structuring our projects this way without even thinking about it.

And with a good reason. That additional layer of abstraction greatly increases reusability, compatibility, scalability, and testability of our software. By abstracting the way our business logic is consumed, we are removing dependencies on the framework we are using. That makes it easier to migrate our code to a different/new technology if needed.

But if we do not need these kinds of benefits, placing business logic inside controllers is a completely viable option. It even makes our code simpler. Less abstraction means less code. Less code, less chance to have bugs.

We do not need some specific way to handle stop of execution. We can simply return a response on any line of code. Business logic placed in service/handlers need a way to handle this scenario. We could return http response directly from service/handler, but then we simply invalidate the purpose of an additional layer of abstraction.

So, viable options are throwing exceptions or returning the result model (result pattern). Both approaches have benefits and pitfalls.

Throwing exceptions keeps code clean, but is expensive. It greatly affects our performance and resource consumption. However, it should not be a concern for most applications. If we are having validation, exceptions should be very rare, and our code could cover only the happy path. When exceptions do happen, in most cases it would indicate we have a bug because the user managed to do something that the system should not support. Or our user has malicious intentions and purposely tries to do something that should not be possible. In both cases that is an exception that needs to be addressed.

Another option is the result model. It tries to fix performance problems with exceptions by simply not throwing them, and instead returning a model with a flag that indicates if the operation succeeded or not. Basically, it is a wrapper around a real model that has field IsSuccess.

This approach forces us to handle not only a happy path, but also failures. First, we need to check if the operation succeeded, and then we can access our true response by accessing the Data field (or whatever we choose to call it). Our code gains additional complexity.

But if we place our logic in the controller, we can simply return NotFound or Forbidden response. We avoid complexity, and do not have any performance penalties. But we now mix business and presentation/infrastructure logic. And we are heavily dependent on the framework we are using. In some cases that is completely OK.

Let’s say we have a Rest API that has only GET endpoints. Data is fetched/processed in background jobs, and we only need to access it somehow. Our endpoint can call the database directly, check if an entity exists, and return appropriate results. What are we gaining by adding that additional abstraction layer here? Do we really need it? Are we ready to heavily depend on the framework we are using? It is up to us to decide.

Conclusion

Users do not care about architecture. It is here for developers, as a last line of defense against complexity we are going to face. It should be a sum of answers to the questions we are asking ourselves while implementing the project. Adjusted to fit our needs, and to make things easier.

If things are getting complicated because we are following some architecture, then we are basically fighting against the rules that we are forcing upon ourselves. And that does not make sense. We should understand the problem we are facing, and solve it. Nothing more, nothing less. And our architecture should reflect that.

Top comments (0)