7

I have a RESTful API service that has three layers: Application/Domain, Infrastructure, and Presentation.

Application/Domain contain my interfaces and models. I currently have three different types of models:

  1. DTO - These are the models my controllers return to the client as well as the models that are passed around all the layers of my application.
  2. POCO - Domain model that has an instance of the corresponding DTO, it has business rules/validations.
  3. Entities - Persistence models that mirror database objects.

Now, if my RESTful API makes a request to another API service and the request requires both a body and response, I would want to create models for both the body and the response right? What would this kind of model be called and which layer would I put it in?

I guess it would have to go in the Application/Domain layer because that's where the interface for my third party API client would also be. But what would I call these models? Are they entities? Are they DTOs?

Hmmm...I guess they could be considered entities. An entity is a model that represents the data in the database and I guess the third party API could also be considered a database of sorts...

I was initially calling the services that connect with dbs repositories and calling services that connect with other APIs ApiClients. But they really are just facade services. So I guess they are the same?

What do you guys think?

brandon d tran
  • 171
  • 1
  • 3

2 Answers2

7

I guess it would have to go in the Application/Domain layer because that's where the interface for my third party API client would also be.

A third party API is, by definition, not part of your current application domain. It is an external service, after all.


Hmmm...I guess they could be considered entities. An entity is a model that represents the data in the database and I guess the third party API could also be considered a database of sorts...

I am sidestepping your "what should I call it?" question because naming isn't quite the main thing to focus on here, but your comment does hit a nail on the head here: external apis are functionally indistinguishable from a database.

In essence, whether you connect to a database server or a web backend which serves data is an irrelevant distinction in terms of how to categorize this logic in your codebase.

As it stands right now, the code calling the API belongs in Infrastructure, just like where your database logic resides.
That being said, depending on size and complexity, you may want to start subdividing the Infrastructure project into separate projects, if that makes more sense to you. In software development, it's always possible that a particular use case may warrant being broken down into smaller parts further.


However, that only established where you put the services that call the API. But where do you put the models for the in/output of your service?

It somewhat depends on whether you are using inverted dependencies or not. If you aren't, then the models can live together with the service in the Infrastructure layer, since your Application will depend on Infrastructure and have access to all of its services and models at the same time.

However, if you're using inverted dependencies, that means that the Application (or Domain) layer defines the service's interface, which needs access to the in/output models, which means that you can't just plop those models in Infrastructure. In this case, they belong to the service's contract, and should be kept closeby to where you store the service interface itself. I personally would put them in the same directory, since they tend to uniquely belong to each other.

That being said, when working on professional (enterprise-grade) projects, I would strongly urge you to separate the interface models from the actual external service models, as a matter of keeping the external service sufficiently abstracted. This ensures that any changes made to the external service (and thus needing to be made to the external service models) don't necessarily have to cause you to change your interface contract itself, as long as you can simply update the mapping between the external and internal models to remain compatible.

Which means:

  • For normal dependencies:
    • One internal (i.e. assembly-private) model in Infrastructure for the service logic
    • One public model in Infrastructure to be consumed by its dependents (i.e. Application)
    • The service class in Infrastructure will map from one to the other as it needs it.
  • For inverted dependencies:
    • One model with the service interface in Application (just like before)
    • One internal (i.e. assembly-private) model in Infrastructure for the service logic
    • The service class in Infrastructure will map from one to the other as it needs it.
Flater
  • 44,596
  • 8
  • 88
  • 122
  • What if, in the inverted dependencies case, the service to be abstracted is very large? Let's say I'm consuming a 3rd party that has a ton of functionality, thus a huge API surface, considering classes, functions, input/output types. Should I abstract all of that into my own interface that lives in an inner layer? It feels like my abstraction will be extremely brittle and tied together with the 3rd party. – Rafael Eyng Aug 22 '23 at 19:15
  • @RafaelEyng: On the face of it you raise a valid point, but you have subtly picked two conflicting stances. If your persistence layer is not the one defining this interface (= the inverted dependencies case), then the interface (and its models) should not be designed based on persistence considerations (which includes knowing what the external API provides and contains). You would be designing the interface and the models based on what your application needs, not based on what the API offers. – Flater Aug 22 '23 at 23:10
  • @RafaelEyng: If your API added a completely new thing, and you want to use it, it is _inevitable_ that you must redevelop. However, if the API changes something and you don't want to change your business logic, then all you have to do is rewrite the Infrastructure mapping in a way that the new API maps to your original model. You can't avoid needing to make changes to Infrastructure when your external components go through breaking changes; but you can _contain_ the redevelopment to the Infrastructure project and nowhere else in your codebase - that's the purpose of designing these interfaces. – Flater Aug 22 '23 at 23:13
  • Thanks, what you said makes sense. I'm still unsure about my case, with Stripe. While I want to have general payment use cases that can call some PaymentRepository or PaymentProvider (implemented at the adapters layer?) that can abstract Stripe away, I'm not sure what to do with my need of having Stripe-specific webhooks. At some point in the webhook flow I am able to make it webhook fall into the general case the PaymentProvider implements, but before that, I need the webhook to have access to Stripe-specific calls and types. So that leaves me clueless about where to put the webhook (...) – Rafael Eyng Aug 23 '23 at 00:02
  • (...) If I implement PaymentRepository at adapters layer, how can it see the Stripe client, that is in the infrastructure layer? Or if I put Stripe webhook in the infrastructure layer, it can access Stripe client in the same layer, but not PaymentRepository to fall into the general flow. But if I put it into the adapters layer, the problem is reversed. The webhook now can access the PaymentRepository, but not the Stripe client. Unless I make a huge interface to cover what I need from Stripe, but when I add the next payment service, fitting the interface would be a nightmare. – Rafael Eyng Aug 23 '23 at 00:07
  • @RafaelEyng: I don't know Stripe, and the fact of the matter is that I shouldn't need to know it in order to help you design your codebase. Instead of basing your design based on what Stripe has, design your application based on what you need it to do and _then_ figure out how to implement those needs using a concrete implementation (in this case it happens to be Stripe). – Flater Aug 23 '23 at 01:28
  • Thanks. I don't disagree with your point. But I do think something, at some point, needs to know the specifics about Stripe. In my case, there are at least 2 points. `PaymentRepository` on the `adapters` layer needs to know if it should call the underlying Stripe implementation to create a payment, or the Google Pay one. And those details should arguably be in the `drivers` layer, but somehow the `adapters` has to acknowledge multiple implementations exist and distinguish between them. (...) – Rafael Eyng Aug 23 '23 at 13:25
  • (...) Also, the Stripe webhook is totally Stripe-specific. It receives Stripe data from Stripe. So I think it should probably live in the `drivers` layer, even though it is arguably similar to a controller (which should live in the `adapters` layer), in the sense that it receives and handles requests. But maybe the difference here is that the requests handled by controllers are agnostic to details while a webhook is by definition attached to a specific service. – Rafael Eyng Aug 23 '23 at 13:27
  • @RafaelEyng: You've introduced the concepts of adapters and drivers here, which were not part of the original question. This requires elaboration. I surmise that "drivers" are the infrastructure implementation and "adapters" is your API, based on what you've said? – Flater Aug 23 '23 at 22:09
  • @RafaelEyng _"something, at some point, needs to know the specifics about Stripe"_ Yes it does. That something is the Infrastructure implementation, **but not its interface**. The implementation has to translate (map) any Stripe specifics to/from your interface models. Your interface should only be designed used models that you designed yourself. These _might_ resemble the Stripe models, or they might not. What's important is that when Stripe decided to change its model, that your codebase is able to maintain its own interface/models without _needing_ to change them (barring breaking changes) – Flater Aug 23 '23 at 22:12
  • Thanks, Flater. I think you are missing my point. Something inside my `adapter` layer needs to know "for this payment, I need to use the Stripe implementation or the GooglePay implementation", even if the 2 implement the same abstract interface. So at some point my `adapter` needs to know "if some parameter is X, I go for the Stripe implementation of that interface, otherwise I go for the GooglePay implementation". I don't see how it can do it without acknowledging that there are 2 different implementations, thus leaking the knowledge of what different "drivers" exist to the adapters layer. – Rafael Eyng Aug 24 '23 at 00:03
  • The best I can think of is to have my `adapter` have a list of all the implementations of the interface, and each implementation returns its own enum value in a `paymentProvider()` method. Then depending on the parameter that determines the payment provider, my adapter picks the correct of the multiple implementations (of course, using it through the interface). That's the only way I can see my `adapter` not having some sort of `if (paymentProvider == 'stripe') { /* pick this implementation, else pick that */ }`. – Rafael Eyng Aug 24 '23 at 00:06
  • @RafaelEyng: The implementation in Infrastructure is not limited to being a single class. You could have an `PaymentRepository : IPäymentRespository` (whose interface is independent of payment providers) _and_ a `StripePaymentProvider` and `GooglePaymentProvider` which are relied upon by `PaymentRepository`'s implementation. But the larger question is first to consider if these providers are an implementation detail or a concrete business requirement, because if it's the latter, then any advice on needing to abstract such an implementation is inherently moot. You need analysis here. – Flater Aug 24 '23 at 00:16
  • 1
    @RafaelEyng: Consider what we're talking about now, and what the actual question that was being asked. This is a **generalized** question, but you repeatedly push back on provided advice by pointing at reasons related to your specific use case. This does not match. You need to analyze whether a generalized answer is actually applicable to your scenario; which requires a level of deep functional and technical analysis that simply cannot be done over StackExchange's intentionally restrictive Q&A format. – Flater Aug 24 '23 at 00:18
-1

The weird thing is, I can't tell the difference between any of your particular Objects, be they DTO, POCO, or Entity.

A DTO is a POCO. Its a data structure containing data. It is the reason why you can send them across a network.

An Entity on the other hand is business logic. Business logic isn't a data structure. It may encapsulate data, in which case why is that being serialised to a repository? It may manipulate data, in which case that Data is in another DTO/POCO.

I think its more accurate to say this:

  • You have DTO objects (just plain old data usually a bunch of named strings and scopes) written in terms of your provided interface, and third-party interfaces. Their job is to serve as the marshalling ground for requests and responses over any given IO system: The API, Third Party Services, The Database, UI, etc...
  • You have Domain objects (just Plain old Data usually in higher level data abstractions like dates, lists, users, etc...) written to support your domain. These are tailored to the algorithms and business logic employed by your own system.
  • You have Domain/Entity objects representing the system that consume, manipulate, and generate Data Structures.
  • You have Presenter objects representing the logic needed to translate between DTO and Domain objects.
  • You have Interactor/Use Case objects whose job is to map a request/response on an API from the incoming DTO through Presenters into an orchestration of Entity behaviours, Domain Data and then back out through presenters to a DTO.

You may want to look into Bob Martin's clean architecture lectures.

Kain0_0
  • 15,888
  • 16
  • 37
  • 1
    _"An Entity on the other hand is business logic."_ Predominantly, "entity" is used to refer to a database model. What you're referring to (business logic) is generally called the "domain object". From a purely English perspective, you're not wrong that "entity" is very vague and pretty much literally just means "thing that exists" and could be applied to almost anything, but from a developer-centric perspective, "entity" tends to imply datalayer. – Flater May 31 '21 at 09:15
  • At an English level yes it pretty vague, I think though it has a much more defined meaning in software. For example: ECS - Entity Component System. It probably depends on the neck of programming you come from. However I am using it coming from the domain/business logic angle. In this case I think the existence of the term ERM (Entity Relational Mapping) undermines your definition. If the Entity lives in the data layer, why is it being mapped from the data layer? Sounds more like the Entity lives above the data layer. In which case the fact that it can be serialised is irrelevant to the Entity. – Kain0_0 May 31 '21 at 09:32
  • I think you're msunderstanding [ERM](https://en.wikipedia.org/wiki/Entity%E2%80%93relationship_model): _"In software engineering, an ER model is commonly formed to represent things a business needs to **remember** in order to perform business processes. Consequently, the ER model becomes an abstract data model, that defines **a data or information structure which can be implemented in a database, typically a relational database**."_ The entities in question represent the data models for the database (which, ideally, mimic the domain objects, but that is not always feasible). – Flater May 31 '21 at 09:54
  • @Flater. Thankyou for pointing that out. Looks like I need to spend some time reading up on this. – Kain0_0 Jun 01 '21 at 00:04
  • _I can't tell the difference between any of your particular Objects_ - honestly, I felt like that when reading the list of 5 types of objects that follow that statement. – Rafael Eyng Aug 22 '23 at 19:21