Separation of business logic through Use Cases

  • Fundamental distinction between pure business logic, application logic, and infrastructure implementation details.
  • Implementation of Use Cases as independent interactors to ensure single accountability and testability.
  • Use of generic interfaces and dispatchers to optimize dependency injection and avoid overloaded controllers.

Separation of business logic through Use Cases

Often, when we delve into the world of development, we come across the term "business logic" It's thrown around casually in any meeting, used to describe everything from a simple if statement on the frontend to complex database management. This ambiguity is no laughing matter, as it can lead to creating systems where everything is mixed up, resulting in that spaghetti code that keeps us up at night when it's time to do an update.

To avoid these headaches, it's crucial to understand that not all decision-making code is created equal. separate responsibilities By using patterns like Use Cases and Clean Architecture, we ensure that the core of your application is robust and independent of whether you use SQL Server, MongoDB, or whether the interface is a website or a mobile app. Let's break down how to build this system so your software is truly scalable.

Deconstructing the concept of Logic

To begin cleaning the code, it's necessary to differentiate the types of decisions the machine makes. business logic It's the one that represents the reality of the real world; these are the rules that an industry expert would give you without knowing anything about programming. For example, that a customer with debts exceeding one thousand dollars cannot request more credit is a pure business ruleregardless of whether it is stored in a JSON or an Oracle table.

On the other hand, we have the application logicwhich is more like the orchestrator. Its function is to set the stage: retrieve the data from the database, call the business rule, and then save the result. This is what some call the "sandwich method", where we have an impure input layer, the pure business core in the middle, and another impure output layer.

Finally, there are the implementation detailsThis is where presentation logic (such as validating that an email has the correct format or deciding whether a button is displayed) and infrastructure logic (technical communication with external APIs or the file system) come into play. These elements are secondary details that should be able to be changed without touching a single line of the system core.

The problem of monster interfaces

In many projects it is common to create a giant service interface, such as a IClientServicewhich contains absolutely every possible action a customer can take. At first, it seems like a good idea because we're following SOLID principles, but over time that class becomes a monster of thousands of lines which is a real ordeal to read and maintain.

The solution lies in winning. greater granularityInstead of a single, massive interface, we can break down each action into a separate class. This is where Use Cases come in: each class represents a unique and specific action that the user can perform, ensuring that each component has only one responsibility.

Implementing Use Cases efficiently

To avoid depending on specific classes and maintain the inversion of control, the ideal solution is to define a generic interface, for example IUseCase<TRequest, TResponse>In this way, we standardize how each use case receives data and returns a response, eliminating the need to create dozens of individual interfaces for each action of the system.

To make this even cleaner, we can introduce a use case dispatcherThis extra layer internally resolves which use case should be executed based on the request, preventing the controller from becoming cluttered with endless dependency injections and freeing up controller code. much clearer.

Advantages of this architectural approach

  • Superior testability: Because the logic is isolated from the database and the UI, you can write unit tests that run in milliseconds without needing to start a server.
  • Technological independence: You can migrate your database or change the frontend framework without the fundamental logic the business is affected.
  • Self-documenting code: Just look at the Use Cases folder to find out exactly. what the app does without having to read the internal code.
  • Real reuse: The same use case can be triggered by a REST API, a scheduled task, or a command console.

Best practices for robust design

To prevent this system from degrading, it is vital that the Use Cases do not depend on each other thus avoiding circular dependencies. It is also recommended that they return domain models and not direct entities from the database, thus maintaining the watertightness of the layers.

Regarding error handling, the most elegant approach is to use result objects (Result Pattern) instead of throwing exceptions to control the flow of business. This makes the code path more predictable and prevents the application from breaking due to errors that were strictly foreseen by logic.

Adopting an interactor-based structure and properly separating the system core from technical details allows the software to be flexible in the face of market changes. By prioritizing the sole responsibility And through decoupling, we transform rigid applications into agile systems that facilitate teamwork and ensure long-term sustainable code quality.


Add as preferred source