1

I am in the process of implementing onion / clean architecture, and would like to understand better how and when to map to my domain entities.

So if we take a specific example where we have a Post and we would like to update a status on this object.

Am I correct in thinking that when I receive data in the controller I would pass this over to the service, at this point the service should first fetch that data from the repository and then map it into a domain entity. It is the domain entity, that will house rules for update, once the operation is performed we would pass this back to the repository.

What since entities !== what we get via the API and what get saved in the DB, what layers should perform that transformation?

3 Answers 3

5

Am I correct in thinking that when I receive data in the controller I would pass this over to the service, at this point the service should first fetch that data from the repository and then map it into a domain entity. It is the domain entity, that will house rules for update, once the operation is performed we would pass this back to the repository.

Generally speaking, yes.

That is assuming that the operation in question requires fetching data to begin with. For e.g. create commands (e.g. your status might just be a new eventsourcing entry), you simply call on the repo to create it. That seems not to be the case for your scenario, but I'm mentioning it to prevent any assumption that you must fetch something from the repo.

how and when to map to my domain entities

what layers should perform that transformation?

There is a spectrum here. Purely theoretically, in order to create the cleanest layer separation, every layer should have its own DTOs which it maps onto.

Using that approach, your controller would map its model to a service DTO which it passes to the service; who in turn maps it to the domain object; who in turn maps it to a repository DTO; and so on.

In reality, layers tend to be relatively thin and often act as a mere pass-through for the simplest of operations (e.g. basic CRUD); and you'd spend more time mapping DTOs than you'd be doing anything else. Because of this, often not every single layer gets its own set of DTOs, simply because it ends up costing more effort than it saves you in the long run.

This is very subjective. The size of your codebase, its complexity, your (or your company's) standard on how clean code needs to be, ... all these factors impact whether or not it makes sense in your scenario to really develop every single layer with its own custom DTOs. Taking the two extremes here:

  • For a enterprise-grade platform where every layer/component has a different team of developers, then definitely consider giving each layer/component its own DTOs.
  • If this is a small personal project with no intention of having persistent maintenance upon release, the extra DTOs have little purpose.

Generally speaking, if you're going to do one DTO mapping, you'd do it on the business layer. This is the most important layer boundary as it separates your internal codebase from your public endpoint (whether it be an API, app, or other).

In a DDD/onion/clean architecture, a DTO mapping between the domain and DAL tends to also be necessary (due to the inverted dependency between domain and DAL). There is some leeway here, but based on your question I infer that there is a need for a DTO mapping here as well.

So at a bare minimum, in your case I would expect a DTO mapping to happen in the service (between the service and the domain), and in the DAL (between the DAL and the domain).

2

When applying DDD patterns it’s common to have a repository per Aggregate. This makes sense, because the Aggregate is a consistency boundary. Evans wrote:

An AGGREGATE is a cluster of associated objects that we treat as a unit for the purpose of data changes. Each AGGREGATE has a root and a boundary. The boundary defines what is inside the AGGREGATE. The root is a single, specific ENTITY contained in the AGGREGATE.

When applying DDD with clean/onion architecture the request flow is typically something like:

  • Controller receives request, forwards request to application service (often after mapping)
  • Application service gets Aggregate from repository, invokes method on Aggregate Root, persists Aggregate

The repository is responsible for constructing the complete Aggregate. Don’t cut corners here, always load the full Aggregate with all child Entities and Value Objects even if you know it isn’t required for the specific use case.

In my experience the most complex part for a repository with a relational database behind it, is change tracking. I would not advise to write your own change tracking mechanism, but rely on an ORM for that even though that often leads to ORM related code leaking into the domain layer. Alternatively use another type of database for storing the Aggregate such as a document DB, and apply CQRS to create separate read-only store(s) for reads. But that’s a whole other topic with it’s own complexities.

2

What since entities !== what we get via the API and what get saved in the DB, what layers should perform that transformation?

Parsing the request, by which I mean taking the representation of the information you get from your general purpose HTTP library/framework and converting it to an in memory representation of the API message, is normally an activity that you want to happen as close to the boundary as practical. So the parser might be invoked by the controller itself, or close to it (if you have multiple HTTP frameworks supporting the same API).

(See Parse, Don't Validate by Alexis King).

The function that you use to convert from the in memory API representation to your domain representation is going to have dependencies on your API definition and domain model definition, so you're probably looking at "application layer" there (based on the dependency arrows; of course there are some cases where the API definition and the domain model definition are in close alignment, so it may be tempting to blur the two -- but keep in mind that API and domain model change for different reasons).

There's a similar argument for the case where you are taking domain values and converting them into your API representation.

For information coming from your persistence store, the basic idea is roughly the same - the primary difference being that your data store is supposed to be under your own control.

The application code will normally access the data via a "repository", which is going to depend on some flavor of "infrastructure code" that knows how to fetch the data that you need, and a domain factory that knows how to take general purpose data into the data structures that you need.

Similarly, you're going to need code that can extract information from your domain model (the entities) and transform that information into the general purpose representations that are understood by the infrastructure component talking to your data store.

A way of thinking of this latter transformation is that you've got some long lived schema (in the sense that we expect it to support many generations of domain model) that describes your data at rest, and you need a function that takes information out of the domain entity and expresses it in this storage form.

So either your domain entity needs to have methods to extract the information in this data representation, OR the entity needs to expose its internal information in a way that supports creating this data representation from the outside.

(In practice, what you'll often see is either (a) an O/RM implementation of the domain entity or (b) some flavor of implicit conversion to a general purpose representation of the information; think JSON mapping.)

Which is a long winded way of saying that we expect the repository to be able to invoke methods for serialization/deserialization of the domain model.

There may be some shock and outrage that the domain model might have dependencies on anything else, but as far as I can tell that doesn't work very well unless you never take information out of the domain model (in which case, what's the point?). In practice, our domain values often include dependencies on general purpose data structures (strings? collections?); the important constraint is to make sure that the dependency arrows point at things that are stable.

Expressed another way - if you are changing your data schema with every release, "onion architecture" is not going to protect you from having a bad time.

1
  • > Expressed another way - if you are changing your data schema with every release, "onion architecture" is not going to protect you from having a bad time. I had never thought of this before, but it makes sense! If I am honest the schema is always changing... what is the solution here?
    – dendog
    Commented Jan 10, 2022 at 13:59

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.