0

I'm trying to implement a project using the Onion Architecture (aka Hexagonal, aka Ports and Adapters, aka Clean Architecture).

I understand that the user interface uses Service interfaces instead of the actual implementations. I also understand that if a Service needs something from an external source (e.g. a database or a Web API) or from another Bounded Context, you want you inject implementations of Infrastructure interfaces.

These two ideas follow the Dependency Inversion Principle (DIP). That's a good thing.

But let's zoom in on the Service layer itself:

Ideally you want most of the logic on the Entities in your model, but there will still be logic that has to be implemented in the Services. In my situation, it is more than I want in one single Service class, so I'm delegating responsibilities to other classes, I'll call them sub-services for now.

And now the question: Would it be wise to apply DIP on all of these sub-services too? They are just internals of the service implementation and would not serve a purpose outside of the domain.

In other words: The official definition of DIP talks about high level and low level modules. Should all (sub-)services be considered one module or are they all separate modules?

2
  • "so I'm delegating responsibilities to other classes" - that's good, you should split things to keep them from doing too much. "Would it be wise to apply DIP on all of these sub-services too?" - apply in what way? If your subservices aren't calling into outer layers, you don't need to do anything special. If they are, they are probably doing so via the objects passed to them by their parent services, so they are already following DIP. Commented Sep 25, 2021 at 6:44
  • "The official definition of DIP talks about high level and low level modules. Should all (sub-)services be considered one module or are they all separate modules?" - a module is a thing that you can consider a single relatively independent unit of some sort. They can be nested. The DIP is more general than the dependency rule in onion architecture - if you have a class that calls into a polymorphic member field not knowing the concrete type of the other class that implements that interface, then those two classes are two modules depending on an abstraction (the interface, in this case). Commented Sep 25, 2021 at 6:44

2 Answers 2

5

You apply DIP when crossing a significant boundary. What's on either side doesn't matter. What matters is having a good reason to keep what's on one side from having a source code dependency on the other side.

The magical thing DIP does is let you enforce that source code dependency rule while allowing flow of control to go in and out of that boundary without resorting to using return.

enter image description here

When organized like this the assumption is that the inner stuff is more stable than the outer stuff. DIP lets you strip off the outer stuff and add new outer stuff without breaking the inner stuff.

When you hear the words "higher" and "lower" understand those terms were developed when the style was to keep all inheritance arrows pointing up. These round diagrams make that metaphor a little meaningless. Here's a diagram that tries to give them meaning again:

enter image description here

This doesn't change your design at all. Just gives those words meaning again.

enter image description here

And now the question: Would it be wise to apply DIP on all of these sub-services too? They are just internals of the service implementation and would not serve a purpose outside of the domain.

I'll stick with the significant boundary rule here. Like anything, DIP can be over applied. I wouldn't be surprised to find every service that faced a significant boundary applying DIP. I would be surprised to find nests of DIP applied to every module in the service layer even if they don't face a significant boundary.

Now that said, circular dependencies in source code are a special kind of hell. DIP can also be used to help avoid that. In all cases, be sure you understand why you're applying it. Don't just apply it cause someone told you to. Know why.

In other words: The official definition of DIP talks about high level and low level modules. Should all (sub-)services be considered one module or are they all separate modules?

The rings in these diagrams depict layers not modules. Now sure, a layer could have only one module in it but it can easily have more. Here a layer is a bag of modules that are asked to follow the layers rules when they touch modules in a different layer.

I can't speak for every author but I have heard Robert Martin explain module to mean object if you happen to be in an object oriented context. Modularization is an older philosophy of designing so parts are easy to remove and swap. You don't have to be object oriented to use modules.

In an OOP context I'd expect one-to-many objects to make up a service and one-to-many services make up a layer (ring).

While Onion, Hex, Ports, and Clean all amount to the same idea they each have their own vocabulary (designed to drive you to different books and blog posts). Because of that be careful when asking these questions because some words mean different things with different authors.

0

And now the question: Would it be wise to apply DIP on all of these sub-services too? They are just internals of the service implementation and would not serve a purpose outside of the domain.

Yes, where applicable.

In general, that is a yes by default, and a no only if you have a valid justification to not do it.

Just because you should rely on DI across bounded contexts, does not somehow imply that you're not allowed to rely on DI within a bounded context.

That's not to say that you must use DI without a bounded context, but if you've already got a DI container up and running anyway, I see no reason to not use it for your entire dependency graph both within and across context boundaries (again, barring very specific and justified fringe cases).
Generally speaking, both the testability and loose coupling benefits of DI tend to outweigh most considerations, making the use of DI a default choice.

In other words: The official definition of DIP talks about high level and low level modules. Should all (sub-)services be considered one module or are they all separate modules?

"Module", much like its sibling "component" is a word that is heavy reliant on the context of granularity. If you're thinking on a different micro/macro level, the definition of what is and isn't a module or component changes. In other words, someone's "high level" is another person's "low level". An architect and a junior developer tend not to operate on the same level, and what one considers a module, another might consider their entire workspace.

In that sense, this question isn't really answerable without getting an idea on the size, scope and complexity of your particular situation.

Overall, I suspect that you may be getting caught up on certain conceptual definitions and are interpreting them as being universally identifiable on every level; which is sadly not the case. Context really matters, and certain architectural implementations only make sense for a given size and complexity of a codebase. Both overkill and underkill are bad ideas.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.