0

Say we have a data grid of car models in our application. In the data grid, many fields were displayed, such as the brand, the model name and the year of production. However there are many more fields of these car model entities stored in the repository (database).

In another place of the application, there is a drop-down list which contains the same collection of car models, but only the model name and its color variants information are used (note the color variants information is not used in the data grid mentioned earlier).

Now what's the best practice to design the contracts?

  1. Have a GetListAsync method which returns paged result of CarModelDtos, which includes any field that can be found in the CarModelRepository. Not a great idea obviously, because we could have many unrelated fields transferred to the frontend, some of which might even bring security vulnerabilities;
  2. The CarModelDto only includes the fields that are used by either the data grid or the drop-down list (the union of them). This still introduces unwanted fields for both of them;
  3. Have two GetListAsync methods in two different application services: one returns only fields that are concerned by the data grid, and the other returns only fields that are interested by the drop-down list. The downside is we have to write two sets of application services and contracts; plus it's not "future proof": if the frontend requests any fields that are not included in these DTOs, we have to make changes to the application services;
  4. Same as 3, but add some "future proof" fields, i.e. the fields we anticipate that are possibly to be used in the frontend in the future. Now the question is, who gets to decide which "future proof" fields to include, based on what kind of rule?
2
  • 1
    5. You could have a single GetListAsync with a single return type CarDTO and apply mappers or projections over the result, from the service itself. Service could support mappers or projectors via DI or via polymorphism. We could approach this like HTTP does, with Content Negotiation. Whether you translate this into more types (DTO) or in flat Maps/Dictionaries is just a matter of implementation details.
    – Laiv
    Mar 10 at 15:24
  • DDD probably doesn't have much to say about this. DDD is a design technique, not a coding strategy. Mar 25 at 13:16

4 Answers 4

4

...plus it's not "future proof": if the frontend requests any fields that are not included in these DTOs, we have to make changes to the application services;...

This is called Reverse Semantic Dependency and is unavoidable in layered architectures. Although the "Business Layer" is supposedly independent of the "UI", it will always have to respond to requirements the "UI" has. Hence you will often find yourself modifying multiple places for a single change.

The right solution is to quit this charade. KISS, YAGNI, etc. Just write down your use-case as is. Repository.displayDropDown() and Repository.displayDataGrid(). Do the things you need exactly as you need them. Now if something changes (display more or less data), you'll likely have this single place to change.

1

There is no universal answer here. There are many possible considerations here. This answer exists of a list of examples/anecdotes as to why different decisions can make sense in different scenarios.

1 I worked for a customer whose domain logic was extremely complicated (think HR/payroll levels of administration). In order to not have the project buckle under the demands of its many customers (A wants it this way, B wants it that way), the company chose to stick by the notion that the backend decides what it serves, and the consumer has to deal with it.
This means that all relevant fields were returned. Different customers did not get different endpoints or different DTOs, simply because the company intentionally refused to add this kind of complexity into its codebase.

2 Another company I worked at sold their project as a solution to customers. Customers needed a highly tailored product. The company actually developed a product that was highly customizable and could easily be reused between customers. However, the pride of the company was its customization.
Here, the opposite decision was made: every customer received their own endpoints and their own DTOs, to help sell the idea that the work was being done with the specific customer in mind.

3 Working on developing a mobile app for delivery services, we were instructed to avoid every byte of mobile data that we could avoid (this is a while ago, when mobile data was anything but cheap).
We ended up developing several very similar endpoints, simply to ensure that we would never have to return any data that wasn't needed.

4 Working in a company which was only responsible for delivering the backend API, not the frontend; a decision was made to simply expose the entire object from the first time. This way, we could avoid a flurry of tickets asking us to constantly evolve the DTOs while the frontend developers kept adding features to their frontend and relying on incrementally more fields than they used to.

5 A company I worked with charged customers for the data they accessed. This cost was counted based both on the amount of requests and which fields were accessed. Therefore, a very custom DTO would be made in order to control exactly what the customer had access to.
This was later changed into a system where the customer could dynamically request which fields to return (and the cost would be tracked on the fly).

In the end, there are several considerations here.

  • If you want to minimize bandwidth usage, custom DTOs help cut down on useless data; but it requires more backend development and entails more reasons for change in the future.
  • If you want to minimize backend development and/or reasons for change in the future, exposing everything at once is a good way to minimize it.
  • If there is access privilege involved here, custom DTOs allow you to strictly regulate content.
0

I'm my opinion, a car is a car: it has a brand, color, model name, year of production, engine, etc. That's the model of your domain. IF it changes it's because your domain changed (or your understanding of it).

All in all, I like @Laiv reply better. You keep your domain safe and have mappers/projections that do the "dirty work" of transforming that car model into whatever the presentation needs.

0

In the data grid, many fields were displayed, such as the brand, the model name and the year of production. However there are many more fields of these car model entities stored in the repository (database).

You are confusing three things here. A classic 3-tier application has 3 different models :

  • presentation model (DTO), used for data exchange with the client
  • business model (DDD), used for business rules validation
  • persistence model (DPO), used for data exchange with the persistence system

Oftentimes, those 3 models are very close to each other, this is why we end up using a lot of mapping libraries. Similar but not always identical. If you use the same model for DTO and any other, you will end up exposing sensible information to the clients, as you noted.

You can use the same model for DDD and DPO if you want, but this will make your application less flexible. Some ORM have hard time with that design. Database will be harder to mock. You will not be able to enforce business rules coming from multiple data sources. You business layer will need to compose with some of the database constraints.

Now what's the best practice to design the contracts?

You have two different UI, with two different data sets to display. This means you should have two different DTO. One for the drop down, another for the grid.

Have a GetListAsync method which returns paged result of CarModelDtos, which includes any field that can be found in the CarModelRepository. Not a great idea obviously, because we could have many unrelated fields transferred to the frontend, some of which might even bring security vulnerabilities;

This is where you get the things wrong in my opinion. Read operations cannot change the application state. Read operations cannot violate business rules. Read operations do not need to go through your domain layer, because the domain layer's purpose is to ensure you do not violate business rules, and this is not needed when querying data. Simply write your DTO, and map data from your database. And if you need more data in your grid later, simply upgrade your DTO and add adequate mapping.

Repository getters are methods used for re-hydrating the entity state from the persistence layer, when you need that data to validate a business transaction. It is not meant for querying, otherwise it would return DTO instead of domain objects.

If you map your database to domain, then domain to DTO, performance will be catastrophic. Also, you will need to have domain objects able to contain all data for your largest queries in multiple instances, consuming lots of RAM. These domain object will be extremely large and complex and will hardly be able to benefit from the database abilities for query optimization. It will provoke large locks on the database when you want to persist any change.

4
  • This answer excludes the possibility of custom access control on read operations. There are cases where the domain can be tasked with filtering the returned data based on the user's privileges. This isn't always just a "403" result - sometimes it requires active censorship/redaction of specific bits of information; which is something that definitely belongs in the domain layer.
    – Flater
    Mar 25 at 10:26
  • If you want to implement row level security, or column level security, you should introduce a security layer between infrastructure layer and its clients (repositories for stage changing operations, mediators for query operations)
    – ArwynFr
    Mar 25 at 10:44
  • When the security rules are straightforward; yes. But that is not a blanket given.
    – Flater
    Mar 25 at 10:52
  • When security rules are not straight forward, this is precisely where you can benefit from having dedicated query mediators. You can implement security directly in your queries, and rules can be different than those in the business services. If they are common for read/write, then add a common security layer.
    – ArwynFr
    Mar 27 at 11:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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