2

System description

This is a simple eCommerce application containing a Products inventory managed by an Admin. The Products in inventory/repository will show in the User App. Admin has the ability to mark ON/OFF a Product. ON = display in User App, OFF = do not display. Products are maintained in a single table (Products) in database.

This system is built (read: attempted) on DDD principles.
The Products data in db is accessed in code via .NET Entity Framework.
There are two different Applications (Admin App and User App) created.

  1. Admin App can see ALL Products.
    1. Domain name = Admin.
    2. Does the simple CRUD operations.
    3. Ignoring other setup here as it is pretty straightforward.
  2. User App should only see "ON" Products.
    1. Domain name = StoreView. (FYI - Entities from Admin domain are not mixed/reused here.)
    2. AggregateRoot = NONE. I realize that UserApp cannot modify a Product. So, decided to create Product as a DTO/POCO class.
    3. Repository = IProductRepository, has GetProducts().

My Problem

Where to place this (ON/OFF) "business logic"? (BTW, this is a business logic, right? OR is it an "application logic"?) According to DDD, business logic should be maintained in the Domain layer. However, this is a "query" (product.Status == "ON"). Where should this logic be put - Domain Entity, Domain Service, Repository or Application?

I found these two questions closest to my problem, but I can't find the answer, alas.
Handling Business Logic/Validation Race Conditions
How to completely avoid business logic in DAL?

Approach I - Query in Repository

Write the query business logic in IProductRepository, e.g.

public IEnumerable<Product> GetProducts()
{
    return db.Products.Where(p => p.Status == "ON").ToList();
}

Disadvantage: This does not look like how to do things in DDD.

Approach II - Query in Application Layer

Get ALL Products from repository, and then apply condition in Application Service/Controller/somewhere like that.

Disadvantage: ALL Products will be loaded in memory and then filtered. Also, I am not sure if this conforms to DDD.

Approach III - Query in an Aggregate Root

Have an AggregateRoot, say, Store. Store contains a property for Products. Get entire Store, including ALL Products from repository, and the "filtering" happens in Products' getter.

ICollection<Product> _products;
ICollection<Product> Products
{
    get { return _products.Where(p => p.Status == "ON").ToList(); }
    private set { _products = value; }
}

Disadvantage: ALL Products will be loaded in memory. Also, keeping ALL Products throughout Store entity's life-time seems really odd.

Approach IV - Maintain two separate Product tables for the two domains

Maintain two separate tables for Products, maybe in two different schemas: admin.Products and storeview.Products.
admin.Products will be populated by Admin users.
Raise a DomainEvent when Status is updated for a Product.
A Domain/Application EventHandler subscribed to the above event will create/update storeview.Products table.
NO query logic required in StoreView domain or "UserApp".

Disadvantage: Additional complexity and infrastructure overhead of maintaining two separate tables in db.

Question

What is the most-appropriate solution, in terms of DDD, strictly?

5
  • 2
    If you weren't writing this in DDD, which approach would you take and why? Jan 26, 2021 at 21:00
  • @RobertHarvey, thanks, I was hoping you would notice this question. That said, I don't want to answer your question because, in all honesty and humility, I don't think I have ever gotten this right in all these years. Ugh. Anyway, to answer your question, if no DDD, I would put it in the "query" to the database, wherever that be, only to avoid load ALL objects in memory.OR, I would load all Product IDs by query, pass it to Business Logic layer, let that layer run the filter and pass them back to App layer, and finally query database to bring whole entities for the filtered IDs.
    – Krishnan
    Jan 27, 2021 at 21:09
  • Hmm.. maybe I misunderstood your question. So... Not thinking about "DDD", I would probably choose Approach 4 as it has the cleanest and most flexible design. But it is complex, no doubt. So until I can buy time to implement it, I would settle for Approach 1 as it will be the most performant.
    – Krishnan
    Jan 27, 2021 at 21:19
  • 1
    I don't understand why you chose to use two separate domains. The products for both use cases / user stories are the same products, so to me the difference in usage for these two users does not justify making it a separate domain. Coupling between domains should be as little as possible and by making this two separate domains, you have the entire product as coupling. I would make it one domain with two functions in an aggregate root.
    – Quido
    Jan 28, 2021 at 10:45
  • Got your point. Consider them as sub-domains if not domains. The primary motivation was that Admin operations are auxiliary to the final Store App. In other words, the Store App can exist independent of how its Products data is populated. Anyway, if I go with your approach and merge them, the question still remains: where should I place that domain query logic? Combining your reasoning to have a single domain and the answer from Fabio below, I feel that I am getting closer to understanding my domain better. I'll add further comments and mark an answer soon. Thx!
    – Krishnan
    Jan 29, 2021 at 4:24

3 Answers 3

4

Decision which product to load/display belongs to business(domain) logic.

As part of the domain, you can introduce a "filter" object which can be passed to the data access layer which will be able to "convert" it to the SQL query.

public class ProductFilter
{
    public string Status { get; }

    public ProductFilter(string status) => Status = status;
}

public IEnumerable<Product> GetProducts(FilterProduct filter)
{
    return db.Products.Where(p => p.Status == filter.Status).ToList();
}

With this approach decision making remains on business(domain) side and we keep database access on efficient level.

Above approach can be used for cases where data access layer is abstracted behind repositories.

For modern ORM frameworks with rich mapping and conversion features(.NET EF Core for instance), is possible to map domain models to the database models. Such frameworks will allow to "skip" on repositories and "treat" ORM as an abstraction, which will allow to load only "active" products without extra level of abstraction.

var products = db.Products.Where(p => p.Status = Status.Enabled).ToList();
1
  • Interesting! Passing filters into Repo functions! I have lots of other questions about your answer. Will try to send them to you in a few days.
    – Krishnan
    Jan 27, 2021 at 21:21
0

Under the circumstances, the difference isn’t so much of domain but of the model. Which is perfect, because that is where you can add a global query filter, in the OnModelCreating event.

modelBuilder.Entity<Product>().HasQueryFilter(p => p.Status=="ON");

If you add that as a global filter for your app, all of your other queries will be exactly the same in both the admin and the app, it’s just that the app will never see any products that are OFF. Except for this one line, it doesn’t need to know that it doesn’t.

https://docs.microsoft.com/en-us/ef/core/querying/filters

0

In my opinion, Infrastructure layer must supply the data 'properly' what means infrastructure must response the filtered data. But "'ON'-Products for User" is a business logic, that is true. You have to express/exploit this in Business Logic in Domain Layer. I would advice, in your domain code getOnProducts must be visible instead of getProducts and Ofc implementation of getOnProducts must be suplied by Infrastructure Layer which means 'Where clause of Status=ON'

Your Answer

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

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