0

Domain context and requirements

So let's say I have the following concepts in my domain:

  • Company: it's just a bunch of data related to a company. Name, creation date, assigned CSA, etc.
  • User: again, name, joining date, etc.

Now the business experts come with a new requirement: we want to show a list of companies in our administration tool and the number of active users. For simplicity reason let's say that "active user" means an user with name containing active (this is made up but I think it simplifies the situation to focus on the problem).

To solve this, the engineering team gets together to domain the model and build a REST API on top of it. Let's see some possible paths. To fit the requirements, the API should return something like this:

[
    {
        "name": "Company A",
        "number_of_active_users": 302,
    },
    {
        "name": "Company B",
        "number_of_active_users": 39,
    },
]

Solution 1

I have my Company aggregate, containing those values:

class Company:
    id: int
    name: str
    creation_date: datetime
    sales_person: str

    def get_active_users(users: List[User]) -> List[User]:
       active_users = [...some filtering here based on domain rules...]
       return active_users
class User:
    id: int
    company_id: int
    name: str

Then I can have repositories for those two aggregates:

class CompanyRepositoryInterface(ABC):
    @abstractmethod
    def get(id: int) -> Company:
        pass

class UserRepository(ABC):
    @abstractmethod
    def get_by_company(company_id: int) -> List[User]:
        pass

Problems

It's highly unscalable. If I need to send N companies or so in every page of the API response, then I would need to fetch every company (N queries), their users (N queries) and compute the values. So I'm facing 2*N queries. Companies will be probably bulk fetched, so it's really N + 1 queries. I could also bulk fetch users, having 2 queries only, but then the code ends up being quite "ugly". Also, probably bulk fetching all the companies users is a little bit slow.

Solution 2

Lazy loading the users in the company aggregate.

Problems

This one actually has the same problems than the first option, because you still would be fetching the user once per company, so N queries. This also has an additional drawback: I would need to use repositories inside the aggregate:

class Company:
    id: int
    name: str
    creation_date: datetime
    sales_person: str

    def __init__(self):
        self._users = None

    def users():
        user_repository = SomethingToInject(UserRepositoryInterface)
        if self._users:
            return self._users
        self._users = user_repository.get_b_company(self.id)
        return self._users

    def get_active_users() -> List[User]:
       active_users = [...some filtering using self._users  nm,-… ...]
       return active_users

Also the aggregate code is more "readable" and domain-centered, containing optimization details.

Solution 3

Lazy loading plus caching the users. This would actually be kind of okay, because N queries to redis is actually pretty fast. Not sure if I would cache every user in a separate key though because we have had problems in the past with slowness in redis is cache values were too big (json caching 1k-2k user information is probably quite big).

Problems

Same than solution 2 but it's faster.

Solution 4

Tell the domain experts that the requirement it's not possible to implement due to too much technical hassle. Instead of that, we will show the number of active users in the "details" of a company. Something like

  • /companies -> return basic company data like name, id, etc, for several companies.
  • /companies/:id-> return basic company data for the company with id=:id
  • /companies/:id/details -> return rest of hard to compute data (like number of active users).

This would imply we also define an additional concept in our domain called CompanyDetails.

Problems

It seems quite hacky. It seems like a domain that have not been fully thought and may be hard to reason about because having Company and CompanyDetails is like having the same concept represented twice in different formats. This approach would solve the above mentioned problems though.

Solution 5

Denormalize the companies table and store a computed version of that attribute. Every user aggregate would be in charge of updating that attribute or probably the company aggregate/repository would be in charge of updating it because the users should probably be created through the company aggregate to keep some others business rules (like maximum number of users allowed, etc).

Question

So how would you model this domain to fit the requirements? If you find some of the things I have written are incorrect, please don't doubt on change my mind!

4
  • "we want to show a list of companies in our administration tool and the number of active users" - where here does it state that you need to return a list of users? They just want the number Apr 3, 2023 at 21:23
  • I know, but they want the number of "active" users. To know if a user is active I need to apply business rules. To do that, I need the user entity. Apr 3, 2023 at 21:35
  • You don't necessarily need the entity - this is in no way required by DDD. What you need is to model a representation of the business rule in your system. That can be done, for example, by having your Company entity come out from the repository with the active users property already assigned. Internally, you can make a query to the database that counts the number of active users, without obtaining the users themselves (for this particular aggregate). Apr 5, 2023 at 12:50
  • I thought about that, but my concern there was retrieving 30 or more companies at the same time. To avoid a huge response time, I would need to code bulk retrieval. So I think I will go with the pre-computed property. Apr 5, 2023 at 14:15

2 Answers 2

1

Now the business experts come with a new requirement: we want to show a list of companies in our administration tool and the number of active users.

It seems, so far, that you are describing a report. In the common case, a report is a description of information that you had at some time in the past. Recall that a human reader is normally sitting about a nanosecond away from the screen, and the data has already taken milliseconds to cross the network.

Therefore, the data in the report is already stale.

Unless you are locking the data against change until people are done looking at it, there is the possibility that a change happens while the data is being viewed.

So you probably need to dig deeper into your requirements here: what's the cost to the business if the information in the report is a days/hours/minutes/seconds out of date?

(For example, if you are designing REST "resources" that communicate the information in this report, that suggests that the business is going to be looking at cached copies of the information).

In cases where stale data is fine, then you don't normally need to worry to much about using the report to design your domain model. So you create whatever reasonably performant implementation of the report you can, and if report latency is unacceptable you look into scheduling updates to a locally cached copy of the report so that you can respond to a GET request with a recently prepared copy.

CQRS (Command Query Responsibility Separation) is a useful search term if you want to research this idea.


When the answer is important for maintaining your domain invariant (aka: our processing of new information is constrained by "number of active users", and using stale data means that the CEO can be sent to prison).... now those concerns begin to impact our implementation of the domain model.

The bad news: the problem is still hard.

The good news: the prospect of jail time (or similar penalties) tends to greatly increase the budget available for getting the details right.

In practice: in just the same way that the report has to cross a network, and therefore the report is milliseconds old when someone looks at it, so to is information that travels to you across the network. So cases where we need new information to be integrated instantaneously are rare.

3

Solution 5 seems closest to how I would imagine doing this.

Domain modeling is mainly for enforcing rules and invariants. But that can often mean that data is in shape that makes them hard to query.

This is then solved by creating a separate read model, that is in shape where such queries are possible and easy. Then, you update that model based on the events that come from the domain.

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.