The biggest mistakes to avoid

cloud-and-code Feb 8, 2025

Invalid corrupted data

Should have a single point of authority that mutate state. - we can do that through a API, we should NOT go through the database because databases are just data stores, they don't validate the entry of data. By hammering straight in the DB we can really break an application because the state/data can be corrupted from there on, and we don't have audit to know who did it.

Solution: define an authority API or command API that makes the state change.


Code driven by CRUD or technicals

By using simply updates on something we don't know what drives the update. We need to drive code by INTENT. By stating what the action is, we can now know the workflow the user was doing to execute that specific operation.

Solution: create commands for the actions that are taking place. The command name will give the EXPLICIT reason why we are arriving to that state. We could have a table which stores commands being executed per request and with this we know how we arrived to that state, or by other words know what was the user workflow without guessing.


Indirection

Most of the times we don't need it, only use them when they provide value. Example ORM backed-up by Repositories. Many times we want to have access to ORM capabilities like ChangeTracking, Lazy-Load and others and not hide that away for the purpose of providing a Repository. If our project is tighly coupled to a ORM like EntityFramework then Repository is just a facade.

Solution: Use indirection when your types don't provide interfaces or way of mocking it. We can create indirections by creating a wrapper class and encapsulate the implementation exposing only the correct methods for testing and mocking. In the repository case we don't need this because we can use Commands instead of Queries and abstract the implementation away from who calls this commands/queries normally with a framework like Moq/FakeItEasy.


Too much generic thinking

Don't do things too much generic. You can do that if you are aiming to provide a Framework for so many clients but if you are working in an in-house company most likely that's not the case and you are adding tech debt to things. This can cause your team to hate the code-base and really annoy delivering functionalities

Solution: Firstly think about delivery simply a functionality. The solution for this is really avoid to suffer in the first hand thinking that you will not suffer later, but as a quote I heard recently: "no matter what we suffer" is kind a true. So don't plan too much ahead and about cases that don't exist, you will save a lot of time and money.


Provide actions users can/cannot do in an API

Most of the times we need to state what the user can/cannot do after an action that he performed. But we don't implement something like HATEOAS out of the box and with normal responses, the client will not know what can/cannot be done.

Solution: Return to your clients in the response actions they can perform. The response can contain the response itself + actions. Actions is an array with {name, uri, and method}.
With this you are stating what your client can do based on the state on the system.
Typically based on the state we can affirm that if a user had done an Order and is still not processed, he can cancel it.
In the actions for this case we provide a CancelOrder action with the name, URI and method. On the UI, if we have actions array with this specific key, we add the button on the interface. The client isn't away about any logic, the logic stays solely on the server and you avoid having duplicated logic.

With this you also gain access to URIs. You don't need your clients to be in sync with the URIs that the API has. As APIs evolve you can evolve your clients with it.


API retuning JSON object instead of Arrays

If you return an array we create a rigid hard way to change our API.
This way we don't have the flexibility to create an evolving API.

Solution: Return JSON objects instead, to leave room for extension in our responses.


Async Workflows

Don't think it about a sequence of actions that take place. Each one can fail independently, but they should be able to run independently. A workflow can be a execution of many operations: as an example, a order is placed, the payment is provided and email is sent. Don't implement this tighly coupled thinking that everything can work, because a payment can fail, and you don't want your order to be placed.

Solution: Use the right frameworks to help to execute commands after a certain event happens. NServiceBus or MassTransit could help. If we got a OrderPlaced event we can execute the payment, and if the paymentSuccess event is created we can send an email. Think about components like if this happens throw an event to a BUS, and let whoever is interested to perform the next actions.


Tags