3

Let's imagine a simple domain model:

// aggregate root
class TaskList(
  id: Long,
  name: String
)

// aggregate root
class Task(
  id: Long,
  taskListId: Long,
  name: String,
  dueDate: OffsetDateTime,
  subtasks: List<Subtask>,
  isDone: Boolean
)

// value object
class SubTask(
  name: String,
  isDone: Boolean
)

I consider a Task an aggregate root since this is the unit of change. It would be pretty uncommon to edit and save the whole TaskList. That would be even more relevant with multiple users and big task lists. As you can see, the TaskList doesn't "own" the Tasks, but a Task knows to what list it belongs.

So far so good. But what would you do if the user should be able to bring the tasks in an arbitrary order that the application should remember (in a database)? The obvious choice would be to maintain a list of tasks in the TaskList object.

class TaskList(
  id: Long,
  name: String,
  tasks: List<Task>
)

However, that would lead to an aggregate root owning another aggregate root what pretty much would ruin the concept of aggregate roots. The best I can come up with is a list of task IDs in the TaskList class:

class TaskList(
  id: Long,
  name: String,
  taskIds: List<Long>
)

How would you model that?

1 Answer 1

1

I would model the TaskList similar to the second approach because it better reflects the domain model from the business perspective. You have a tasklist with a bunch of tasks in it. Having the task know to which tasklist it belongs better fits to the data model (e.g. in a relational database).

But I would suggest some minor change that from my experience introduces more consistency with strong typing:

class TaskList(
  id: Long,
  name: String,
  tasks: List<TaskId>
)

The TaskId is a value object representing the id of the task. With that your code is more readable and all your domain logic code does not need to know about the primitive datatype that is finally used to represent an id. If you change the type of the id from long to string this only needs to be considered by the TaskId value object and your persistence logic with reduced amount of change.

I have used this approach in practice and so I also want to share some insight:

Be sure to always update the list of tasks (here task ids) as a whole when sending it from the client to the backend. This is an easy approach which is in most cases sufficient and solves race conditions where several clients might change the position of a task in the list. So with this approach the latest change always wins but there are measures to ensure the client that it will operate on out-dated information which will even mitigate this. Such as using ETags in REST...

If you need something more complex such as ordering the tasks based on priorities rather than relative to each other you can even come up with another value object (you will sure find a better name based on your domain knowledge, but I hope you get the idea)

class PrioritizedTask(
  id: TaskId,
  priority: TaskPriority
)

which you can then use in your TaskList class

class TaskList(
  id: Long,
  name: String,
  tasks: List<PrioritizedTask>
)

In this case you can also update a single task inside a list without the need to send the hole task list to provide the required ordering as the ordering is calculated on demand when requesting or displaying the task list based on the ordering of the priority. So if another client now changes the priority of another task in the list at the same time it does not interfere with the first change...

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.