5

I am trying to understand which of the following two options is the right approach and why.

Say we have GetHotelInfo(hotel_id) API that is being invoked from the Web till the Controller.

The logic of the GetHotelInfo is:

  1. Invoke GetHotelPropertyData() (Location, facilities…)
  2. Invoke GetHotelPrice(hotel_id, dates…)
  3. Invoke GetHotelReviews(hotel_id)

Once all results come back, process and merge the data and return 1 object that contains all relevant data of the hotel.

Option 1:

  • Create 3 different repositories (HotelPropertyRepo, HotelPriceRepo, HotelReviewsRepo)

  • Create GetHotelInfo usecase that will use these 3 repositories and return the final result.

Option 2:

  • Create 3 different repositories (HotelPropertyRepo, HotelPriceRepo, HotelReviewsRepo)

  • Create 3 different usecases (GetHotelPropertyDataUseCase, GetHotelPriceUseCase, GetHotelReviewsUseCase)

  • Create GetHotelInfoUseCase that will orchestrate the previous 3 usecases. (It can also be a controller, but that’s a different topic)

Let’s say that right now only GetHotelInfo is being exposed to the Web but maybe in the future, I will expose some of the inner requests as well.

And would the answer be different if the actual logic of GetHotelInfo is not a combination of 3 endpoints but rather 10?

Software Engineer
  • 15,457
  • 7
  • 74
  • 102
Sash
  • 271
  • 2
  • 14

3 Answers3

4

You can see a similar method (called Get()) in "Clean Architecture with GO" from Manato Kuroda

Manato points out that:

  • following Acyclic Dependencies Principle (ADP), the dependencies only point inward in the circle, not point outward and no circulation.
  • that Controller and Presenter are dependent on Use Case Input Port and Output Port which is defined as an interface, not as specific logic (the details). This is possible (without knowing the details in the outer layer) thanks to the Dependency Inversion Principle (DIP).

https://miro.medium.com/max/1053/1*mTIKd9Vf0l7Sg7oXhmamQw.jpeg

That is why, in example repository manakuro/golang-clean-architecture, Manato creates for the Use cases layer three directories:

  • repository,
  • presenter: in charge of Output Port
  • interactor: in charge of Input Port, with a set of methods of specific application business rules, depending on repository and presenter interface.

You can use that example, to adapt your case, with GetHotelInfo declared first in hotel_interactor.go file, and depending on specific business method declared in hotel_repository, and responses defined in hotel_presenter

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • I red this article, indeed very good article. So correct me if I am wrong but you actually saying that option 1 is the way to go. – Sash Mar 22 '20 at 17:56
  • @Sash That would be the closest, yes. But I would try and adapt the repository/presentor/interactor model in the usecase layer. – VonC Mar 22 '20 at 17:58
2

Is expected Interactors (Use Case class) call other interactors. So, both approaches follow Clean Architecture principles.

But, the "maybe in the future" phrase goes against good design and architecture practices.

We can and should think the most abstract way so that we can favor reuse. But always keeping things simple and avoiding unnecessary complexity.

And would the answer be different if the actual logic of GetHotelInfo is not a combination of 3 endpoints but rather 10?

No, it would be the same. However, as you are designing APIs, in case you need the combination of dozens of endpoints, you should start considering put a GraphQL layer instead of adding complexity to the project.

Anderson Marques
  • 808
  • 8
  • 13
1

Clean is not a well-defined term. Rather, you should be aiming to minimise the impact of change (adding or removing a service). And by "impact" I mean not only the cost and time factors but also the risk of introducing a regression (breaking a different part of the system that you're not meant to be touching).

To minimise the "impact of change" you would split these into separate services/bounded contexts and allow interaction only through events. The 'controller' would raise an event (on a shared bus) like 'hotel info request', and each separate service (property, price, and reviews) would respond independently and asynchronously (maybe on the same bus), leaving the controller to aggregate the results and return them to the client, which could be done after some period of time. If you code the result aggregator appropriately it would be possible to add new 'features' or remove existing ones completely independently of the others.

To improve on this you would then separate the read and write functionality of each context into its own context, each responding to appropriate events. This will allow you to optimise and scale the write function independently of the read function. We call this CQRS.

Software Engineer
  • 15,457
  • 7
  • 74
  • 102
  • The problem is that the there is a lot of logic in the aggregation part and that's why I don't think it should be in controller because the controller layer shouldn't contain business logic. That is why I put the aggregation logic in a dedicated use case. The question is should this use case "GetHotelInfoUseCae" orchestrate other use cases or orchestrate directly repositories. – Sash Mar 22 '20 at 17:42