Reading Time: 9 minutes

Today we’ll touch on a topic that’s been on my to-do list for a while. I’ve recently watched a lot of videos on domain modeling in DDD. Unfortunately, many of them discuss trivial or well-known issues. Below, I’ll describe a problem you’ve certainly never encountered before (and neither have I). We’ll try to break it down, first at a high level and then by constructing a deep domain model. Join myself!

Problem description (brief)

  • The system settles shared expenses incurred within a single location (apartment, office, property).
  • Settlements are calculated in defined settlement periods, typically monthly.
  • Each period may have a variable number of participants.
  • A participant may join or leave during a settlement period.
  • Cost responsibility is based on actual participation time, unless a cost defines a different rule.
  • Each cost has a monetary amount, date, category, and an associated allocation rule.
  • Costs may be recurring, consumption-based, or one-off.
  • A cost may apply to:
    • all participants,
    • a selected subset,
    • participants with explicit exclusions or limits.
  • The system supports multiple cost allocation rules, including:
    • equal split,
    • proportional split using predefined weights,
    • time-based (prorated) split,
    • consumption-based split,
    • combinations of multiple rules.
  • Allocation rules may change over time.
  • Cost allocation may produce rounding differences, which must be handled according to a defined policy.
  • Costs may be added after a settlement period has ended.
  • Cost corrections affect future settlements according to predefined correction rules.
  • A settlement period may be closed, after which results are considered final.
  • For each participant, the system calculates either:
    • an amount owed, or
    • a credit balance.
  • Settlement results must be transparent and traceable, allowing each participant’s amount to be explained down to individual cost items.

Defining model components

To begin designing the model, let’s draw its components. The model will consist primarily of Sattlements. In our system, this is synonymous with the word “obligations.” We also have resource users. In our contextual language, this would be “perticipant.”

This is a basic understanding of components. Notice that there are no relationships or fields yet, as this isn’t an ERD (Electronic Data Model). Let’s visualize the problem. Let’s illustrate it from a different perspective—a temporal perspective:

This chart better illustrates the flow over time. Our timeframe is “Period X.” During this period, participants (A, B, C) incur costs. We can tentatively assume that these are people renting space in the open-plan space. I think this is a pretty good use case that will help us visualize the problem.

Our software is therefore designed to manage the rental of a given resource between participants (lessors). As we can see, the beginning and end of the commitment for each participant are determined individually (there are periods when someone incurs no costs, but there are periods when the rental is continuous. Let’s imagine that one person comes to the office for 20 days out of 20 working days, another comes in on a break, etc.

Event storming – one of the techniques for learning about the problem

Let’s try to break it down using sticky notes. Alberto Brandolini, the creator of ES, came up with a way to explore problems during software design. You can find more details here. I’m no expert in this technique, but I know the basic concepts and will try to prepare an ES board at the Problem Exploration (Big Picture) level.

This is what a list of domain events would look like. We can:

  • open the period
  • add cost
  • participant can join
  • the participant may leave
  • the period may end
  • the period can be settled (closed)
  • liabilities can be converted
  • an invoice can be sent to participants

Types of cost allocation

The event timeline helps us identify the most important events during the billing period. This gives us an overview of what happens during the billing period. Now let’s look at how to solve allocation rules, one of the key elements of billing:

  • equal split – costs are divided equally between participants
  • proportional split using predefined weights – each participant has its own weight assigned to a given cost type
  • time-based (prorated) split – division between participants depending on the time each of them used a given resource
  • consumption-based split – division based on the proportional consumption of the resource by participants (regardless of the time of use)
  • combinations of multiple rules – the most difficult – although from a high-level level it doesn’t matter that much

When implementing the model, we’ll need to implement an appropriate solution to this problem, but this isn’t a problem we’ll address in this article; implementation will be covered in a separate post. Let’s just get this out of the way for now.

Cost structure

The cost should consist of several components:

  • Monetary amount – value written as number + currency
  • Date – the date on which the cost was charged
  • Category – expense category (electricity bill, desk use, microwave use, etc.)
  • Allocation rule – known above – includes the method of dividing the cost between participants

I think we’ve created our first Domain Entity this way. The next one will definitely be the Participant. A Participant will be a simple entity composed of a Name, Email Address, and ID. For this module, we don’t need anything more. Each participant will also have their own account, where their billing history will be saved. Let’s call this the Participant Account.

The next entity will likely be the Settlement Period. This will represent a time period (specifically, settlement months). Its properties include the start and end of the period, as well as its status (open/closed).

After this basic recognition, we can freely draw the supposed shape of the entity.

I’ve intentionally marked the Invoice entity in a different color. It’s not included in the requirements, but it’s a natural consequence of the application’s operation. Let’s now try to describe the entity’s components and the relationships between them in more detail. This sketch is intended only to outline and sequence the subsequent building blocks. Even at this stage, it’s clear that the Invoice entity definitely doesn’t fit in with the rest because:

1. It refers to events after the closing of the billing period.
2. It can be issued independently of other events; it is a consequence but not part of the process.

However, do not discard the Invoice until we are sure it is a different Bounded Context.

Pseudo ERD

Below you’ll find the next stage of design: designing entities and their relationships, which we can tentatively call “Pseudo ERD.” Entity Relationship Diagram (ERD) – this concept is associated with databases, but note that so far we haven’t mentioned the persistence layer. I haven’t mentioned any database engine, framework, or ORM, as that’s not the modeling cue. Technical decisions will come later. So let’s look at the ERD:

Here we see a more detailed view. We also see that all entities are dependent on the Settlement Period to some extent. Let’s now look at the central entity, the Shared Expanse. This is a central location in the schema because it contains:

  • cost types
  • cost allocation types between participants
  • the divisible amount (SharedAmount)
  • SettlementPeriodId – a reference to our AggregateRoot.

Building blocks from DDD

I’m starting to use concepts strictly related to DDD, so I think it’s worth briefly describing the basic building blocks in tactical DDD (yes, we’re already on the borderline of tactics – the only strategic decision we have is whether to include Invoice in our context or whether Invoicing will be another one).

Entity

An object with an identity that is more important than its current state. It may change attributes over time, but it remains the “same” entity.

Example of meaning: order, user, subscription.

Value Object

An object without identity, defined solely by its values. It is immutable and compared by value equality.

Example of meaning: amount of money, date range, email address.

Aggregate

A consistent transaction boundary grouping entities and value objects. Protects business invariants.

Aggregate Root

The only gateway to the interior of the unit. Only through it can the unit’s status be modified.

From the outside, you never operate on internal entities directly.

Repository

Abstraction for accessing aggregates.

It mimics an in-memory collection, not a DAO or Active Record.

It retrieves and stores Aggregate Roots, not random entities.

Domain Service

Domain logic that::

  • does not naturally fit into a single entity
  • operates on multiple aggregates
  • does not maintain state

It’s still a domain, not infrastructure.

Domain Event

An event describing something that has already happened in a domain. It is used for communication and response to state changes.

It is not a command or a request.

Factory

It’s responsible for creating complex aggregates. It hides complex initialization logic and monitors invariants from the start.

Specification/Policy

It encapsulates a business rule or condition. It can be used for validation, filtering, and decision-making.

A clear way to write complex conditional logic.

Module (Package)

Logical grouping of domain elements.
Helps maintain model consistency and readability.

Not to be confused with bounded context—that’s a level below.

The model is incomplete

I feel that the direct relationship between SettlementPeriod, SharedExpanse, and Participant could be encapsulated in a mini-aggregate called ParticipantExpanse, which would act as a bridge between a single cost participant and the entire settlement period. Furthermore, since we don’t have any reference to the shared resource, it would be worthwhile to group this into ParticipantExpanse. The next iteration of our model would look like this:

A lot has changed. First, the (C) notation appeared, which stands for collections of objects. From a relational perspective, one entity contains a collection of other entities. I also added (VO) – Value Object. This is one of the DDD building blocks described above.

Moving up, I also marked the scope of our aggregate: a coherence unit that aggregates multiple entities and ensures coherence during business operations. I also marked the Aggregate Root, which in our case is SettlementPeriod. The aggregate can only be accessed from the outside by accessing the Aggregate Root. This is very useful for preventing logic from spilling over, and it helps ensure encapsulation. There was something about this in college back in the day, right? 🙂 I also removed Participant and Facility from the aggregate.

Our model should only contain references to them, but it doesn’t operate strictly on participants or the Facility. A different BC or even a different module will likely be dedicated to user management, and a separate aggregate will certainly ensure its coherence. The same applies to Facility – we only want the essential elements in our aggregate; a reference to FacilityID is sufficient; we shouldn’t care where the Facility is located or what it’s called. From the perspective of our model, this isn’t important.

I went a step further and also added a framework for Bounded Context, which I called Settlements. I decided not to include Invoice in it – after analysis and a short modeling session, I already know it will be a separate BC. Perhaps even a separate module. Who knows?

Everything is fine, but how do we code it now?

As I’ve noticed, our model is now complete, or at least it seems so. I won’t show the implementation in this article. I’ll devote the entire second part to that, but as a sort of introduction, I’ll offer a UML class diagram. So far, we’ve focused on nouns—entities. Now, to make it more functional, we’ll add verbs to UML, meaning processes and actions—class behaviors.

Note also that we’re missing the so-called input state. Another BC will be responsible for its formulation: Facility Management, where you can add Resources to Facilities and Participants to Facilities. This will likely be a standard CRUD with adding, editing, and deleting. We can imagine it as an administration panel for managers where they can enter all this data. Our module will only respond to DomainEvents such as:

  • SharedExpanseCorrected
  • ParticipantJoined
  • ParticipantLeft
  • SettlementPeriodStarted
  • SettlementPeriodClosed

In our context, we assume that such a connection already exists and we only read it (Read Model) to construct our aggregate and manage its behavior and consistency.

Class Diagram – created in Mermaid

The diagram above is just a sketch; once I start coding, a lot of it will probably change. Expect the second part of this post soon. In the meantime, I’ll leave you with these conclusions and encourage you to share your thoughts in the comments.

The problem initially seemed quite trivial, but the deeper we go, the more complicated it becomes. In reality, we’re still a long way from writing the code. We have a concept, we have diagrams, but the code has to work. Maybe creating an application that solves this problem would be a good idea. I don’t know, I’ll see how it develops in the coming days. For now, I think we’ve figured out the model itself quite well. But as is often the case with programming, the devil is in the details.