#Should Business Logic Orchestration Be Handled by Controllers or Services?

12 messages · Page 1 of 1 (latest)

normal lagoon
#

My Current Approach (Controller-Oriented Orchestration)

  1. DTO validation by param decorators (using zod with a custom interceptor)
  2. The controller injects all necessary services and coordinates between them to perform the task.
  3. No other service or controller depends on the controller, eliminating the risk of circular dependencies.

Pros:

  1. Clean and flat dependency graph.
  2. Services remain small, focused, and independent.
  3. Avoids circular dependencies entirely.

Cons:

  1. Potential code duplication if the same orchestration logic is needed across multiple controllers or entry points, though in my experience, this hasn’t really happened.

CTO's Preferred Approach (Service-Oriented Orchestration)
My CTO recommends moving orchestration into the service layer. The controller should only:

  1. Handle request validation/parsing.
  2. Pass DTOs to a service.
  3. Let the service handle coordination between other services.

Pros:

  1. Encourages service reusability across controllers, other services, and consumers.
  2. Keeps controllers thin and focused on HTTP concerns.

Cons:

  1. Risk of cyclic dependencies - e.g., ServiceA calls ServiceB, and ServiceB also needs to call ServiceA.
peak shale
#

Hi Bhupen Pal, how is moving orchestration
const a = await serviceA.a(); const b = await serviceB.b(a)
to another place creates circular dependency?

latent meteor
#

Controllers should only handle transport related concerns

Services should handle business rules

Repositories should handle data access and perisstence


If you have circular references between services, you need to rethink the architecture.

#

That's separation of concerns 101 and the whole reason the industry even adopted the terminology

normal lagoon
normal lagoon
# latent meteor Controllers should only handle transport related concerns Services should handl...

Please correct me if I am wrong:

  1. Controllers can only call a single service of their own module/domain (e.g. UserController can only call UserService).
  2. Service then handles the business logic and make the repository calls (But what if ProductService needs data from OrderService and OrderService requires data from ProductService).
  3. Repository only handles database operations.

I understand the req, res context should only be available till Controller, services should be provided with valid data by the controllers.
But what about scenario described in the point 2?

I am open to learning, it would be great if you could expand a bit more on "If you have circular references between services, you need to rethink the architecture."

final dock
# normal lagoon Please correct me if I am wrong: 1. Controllers can only call a single service o...
  1. I'd say this is an issue that occurs when you start grouping by data instead of behaviour. So when you get into a circular dep like this, you should ask yourself if this shouldn't be its own module.

You can always move the logic up into a shared kernel and use it that way, or expose some kind of public api from a module and use that instead(but this doesnt really make sense unless you are really working with domains as modules)

A one way dependency is fine, not every module has the same importance. This might lean into loosely coupling (might be a fun read)

My two cents anyway

  1. Doesn't have to be. It just gets/saves data from something and returns it.
latent meteor
#
  1. controllers are responsible for gathering all necessary data for the client to display and trigger any operations the client requests. They can inject and call multiple services, but they should not contain logic related to business decisions.

For example:
It can read data from one service and call operations on another two setvices. But it shouldn't make decisions based on the results (except of course checking for null)

#

There's also no limitation to how many layers of busines logic there is. There can be two or three levels of service (some for shared logic, as donny mentioned)

normal lagoon
# latent meteor 1) controllers are responsible for gathering all necessary data for the client t...

From what you've explained, I'm basically doing the same thing. My controller doesn't hold any business logic, it just makes sure things happen in the right order. It gets data from services, checks if anything’s missing or null, and then passes that data to the next service when needed. So, it acts more like a coordinator than anything else.

Please feel free to point out any mistake or anti-pattern that you see.

latent meteor
#

If the "orchestration" logic gets more complicated, you can extract it to a higher-level service.