# Why We Stopped Keeping Business Logic Inside API Controllers


**Building Droplox — Engineering Notes #5**  
  
At the beginning of almost every application, API controllers are remarkably simple.  
  
They receive an HTTP request, validate the input, invoke a few methods, persist changes to the database, and return a response to the client.  
  
At this stage, everything feels perfectly reasonable. The controller seems like the natural place for this kind of logic.  
  
But as the product evolves, this approach gradually begins creating problems.  
  
If you haven’t read the previous article in our engineering series, it provides useful context for understanding how our architectural thinking at Droplox has evolved over time:  
  
[https://droplox.hashnode.dev/how-we-realized-our-initial-product-catalog-model-was-no-longer-enough?utm\_source=hashnode&utm\_medium=feed](https://droplox.hashnode.dev/how-we-realized-our-initial-product-catalog-model-was-no-longer-enough?utm_source=hashnode&utm_medium=feed)  
  
**How a Controller Gradually Becomes the Center of the Entire System**  
  
As the platform grows, every new business workflow introduces additional requirements.  
  
We need to:  
  
Verify user permissions.  
Ensure an order is in a valid state.  
Update inventory levels.  
Record an audit trail.  
Publish events to other services.  
Perform additional validation.  
Handle failures gracefully.  
  
Individually, each responsibility seems small.  
  
And almost every time, the thought is the same:  
  
*“It’s easier to add just a few more lines to the controller.”*  
  
Then it happens again.  
  
And again.  
  
Eventually, a controller that once contained only a few dozen lines becomes one of the most complicated files in the entire project.  
  
But size is not the real problem.  
  
The real issue begins when a single component becomes responsible for far too much.  
  
**When One Class Knows Too Much**  
  
Over time, the controller stops being just an entry point.  
  
Instead, it begins to:  
  
Receive HTTP requests.  
Validate incoming data.  
Execute business rules.  
Interact with the database.  
Communicate with external services.  
Build responses for clients.  
  
Every new feature touches multiple layers of the application.  
  
As a result, code reviews become more difficult.  
  
Testing requires more effort.  
  
Reusing existing business logic becomes increasingly challenging.  
  
Worst of all, business processes begin depending on the behavior of a specific HTTP endpoint.  
  
That was the moment we realized the problem wasn’t controllers themselves.  
  
The problem was the amount of responsibility we had gradually assigned to them.  
  
**The Question That Changed Our Approach**  
  
At one point, we asked ourselves a very simple question.  
  
**If REST APIs disappeared tomorrow and were replaced by another interface, should our business logic have to change?**  
  
The answer was obvious.  
  
**No.**  
  
Business processes should never depend on how a command reaches the system.  
  
REST.  
  
GraphQL.  
  
A message queue.  
  
A CLI.  
  
A background job.  
  
These are simply different ways of delivering commands to the application.  
  
The domain rules should remain exactly the same regardless of the transport mechanism.  
  
That realization led us to gradually move business logic out of controllers and into dedicated components responsible for executing specific use cases.  
  
**The Controller Returned to Doing Only Its Job**  
  
After the refactoring, the controller’s responsibility became dramatically simpler.  
  
Today, its job consists of only three steps:  
  
Receive the request.  
Delegate it to the appropriate use case.  
Return the result to the client.  
  
Almost everything else happens outside the HTTP layer.  
  
The controller no longer makes business decisions.  
  
It simply connects the external interface to the application’s internal logic.  
  
At first glance, the change appears relatively small.  
  
In reality, its impact reached much further.  
  
**More Than the Code Changed**  
  
As the architecture evolved, so did the way our engineering team discussed software.  
  
Previously, we often asked:  
  
*“What else needs to be added to this controller?”*  
  
Today, our conversations are completely different.  
  
Instead, we ask:  
  
Which business process does this use case represent?  
Where should responsibility for this business rule belong?  
Can this use case be reused elsewhere in the platform?  
  
At first glance, the difference may seem purely semantic.  
  
In practice, it fundamentally changes how architecture is designed.  
  
The team stops thinking in terms of HTTP requests.  
  
Instead, we begin thinking in terms of business processes.  
  
**What Changed in Practice**  
  
Once responsibilities were separated, many aspects of development became much simpler.  
  
The same use cases can now be reused throughout different parts of the application.  
  
Business logic is no longer tied to REST APIs.  
  
Testing became easier because validating business rules no longer requires spinning up HTTP infrastructure.  
  
When a new communication channel is introduced, we don’t duplicate existing logic—we simply reuse it.  
  
The result is a far more flexible architecture that adapts much more easily to change.  
  
**This Principle Turned Out to Be Valuable Beyond APIs**  
  
Later, we applied the same thinking to other parts of the platform.  
  
For example, while designing our internal tools.  
  
Over time, it became increasingly clear that architectural quality is not determined by the number of modern technologies or fashionable design patterns.  
  
What matters far more is understanding which component owns which responsibility—and where the boundaries between architectural layers should exist.  
  
We explored this topic in greater depth in another engineering article:  
  
[https://dev.to/droplox/why-internal-tools-deserve-the-same-thoughtful-architecture-as-customer-facing-products-5cia](https://dev.to/droplox/why-internal-tools-deserve-the-same-thoughtful-architecture-as-customer-facing-products-5cia)  
  
**A Controller Is an Entry Point—Not the Center of Business Logic**  
  
Today, we view controllers very differently.  
  
They should not make business decisions.  
  
They should not contain domain rules.  
  
They should not understand the application’s internal implementation.  
  
Their responsibility is much simpler:  
  
Receive a request.  
  
Delegate it to the appropriate use case.  
  
Return the response.  
  
The less a controller knows about business processes, the easier the system becomes to maintain, test, and evolve over time.  
  
**The Question We Ask Ourselves Today**  
  
Whenever we design a new capability, we ask ourselves the same question.  
  
**If REST APIs disappeared tomorrow, how much business logic would we need to rewrite?**  
  
If the answer is:  
  
**“Almost all of it,”**  
  
then our architecture is still too tightly coupled to the transport layer.  
  
If the answer is:  
  
**“Almost none,”**  
  
then responsibilities have been separated correctly.  
  
That is exactly what we strive for today.  
  
**Conclusion**  
  
After years of building software, we arrived at a somewhat unexpected conclusion.  
  
The best API controllers are usually quite boring.  
  
They contain very little business logic.  
  
They make almost no business decisions.  
  
They know almost nothing about the application’s internal processes.  
  
And that is precisely what makes them so valuable.  
  
When controllers focus only on their intended responsibility, applications become easier to test, simpler to maintain, and safer to evolve as new requirements emerge.  
  
Ultimately, great software architecture is built around **business processes**, not HTTP requests.  
  
Everything else is simply a way of interacting with the system.  
  
⸻  
  
**Further Reading**  
  
**How We Realized Our Initial Product Catalog Model Was No Longer Enough**  
[https://droplox.hashnode.dev/how-we-realized-our-initial-product-catalog-model-was-no-longer-enough?utm\_source=hashnode&utm\_medium=feed](https://droplox.hashnode.dev/how-we-realized-our-initial-product-catalog-model-was-no-longer-enough?utm_source=hashnode&utm_medium=feed)  
  
**Why Internal Tools Deserve the Same Thoughtful Architecture as Customer-Facing Products**  
[https://dev.to/droplox/why-internal-tools-deserve-the-same-thoughtful-architecture-as-customer-facing-products-5cia](https://dev.to/droplox/why-internal-tools-deserve-the-same-thoughtful-architecture-as-customer-facing-products-5cia)
