sequent icon indicating copy to clipboard operation
sequent copied to clipboard

Considerations for adding a -for me- missing concept of Entities.

Open berkes opened this issue 4 years ago • 3 comments

I think I'm missing Entities in my aggregates.

A bounded domain, as described in DDD, is made up of Aggregates, Entities and Value Objects. One thing I like about Sequent is that is offers immutable Value Objects next to Aggregates.

My domain, however, needs something in between the two: I have a Timeline which has things like name, acces, an ID, etc. But also has a list of Steps. Each step is "long lived", and can be modified through the Timeline. E.g. "activate_step(step_id)" or "reorder_steps(ordered_step_ids)". It has an identity that supersedes it's attributes - so It is not a value object; eventhough I'm currently abusing a value object for this. a Step on it's own makes little sense: the invariants and business-requirements are about the entire set (which is the Timeline). E.g. "activating a step, deactivates any other". etc. Since a Step cannot stand on its own with a history and events, it is not an aggregate. But since a Step needs an ID which defines its identity, it is not a value object either.

The way I would like it, may help sequent too, hence this proposal, before I spend time on a PR. Please let me know if you think this fits the project and/or if it needs additional thought and ideas or if it is something I should keep in my application instead.

In DDD, an aggregate is often referred to as a collection of entities with one root entity. That Root entity acts as the identifier for the Aggregate and has a global ID. Any other entity has an ID that is local to the aggregate.

This, however, changes what sequent has as Aggregate so significantly, that I think a version with a simple twist could work as well. Here an Entity is something in between a Value object and an Aggregate. An entity:

  • Has an ID (a uuid) - technically other aggregates or bounded domains could reference this, but this is not encouraged.
  • Is instantiated with id, attributes.
  • Has no events or history.
  • Is equal to another Entity if both the ID and its attributes are equal.
  • Is immutable.
  • Is included when marshalling for snapshots.
  • cannot be used as type in commands and events.
  • has a replace() method for ergonomic updates to attributes.
  • Has no other behaviour and is side-effect-free.

A pseudocode version:

class Sequent::Entity
   include Sequent::Core::Helpers::AttributeSupport
   attr_reader :id

   def initialize(id, attributes)
      @id = id
      update_all_attributes attributes
   end

   def replace(new_attributes)
      self.class.new(id, attributes.merge(new_attributes.compact))
   end
end

Edit: immutability is technically not needed, I guess, but it makes it much easier to reason. And it clarifies that an Entity should not have behaviour to update itself. e.g. should not have methods like #activate(), which would set a state attribute or such. This should, If I understand the DDD/CQRS correct, best be kept in the aggregate.

Are others encountering this concept as missing from sequent too? And if a PR is welcome for this, what would you want included? Should I e.g. include it in the documentation/manual; iirc the value-objects are not in there either.

berkes avatar Dec 29 '21 14:12 berkes

Hi @berkes ,

This sounds like a good addition. We are also currently using ValueObjects with an id for this. But I think it is good to have this as a concept in Sequent. Some feedback on your thoughts:

In DDD, an aggregate is often referred to as a collection of entities with one root entity. That Root entity acts as the identifier for the Aggregate and has a global ID. Any other entity has an ID that is local to the aggregate. This, however, changes what sequent has as Aggregate so significantly,

Either way I don't think, or see how, that will change what Sequent has as an Aggregate significantly. It is, to me, an implementation detail.

that I think a version with a simple twist could work as well. I fully agree, we could relatively easy implement this in Sequent (like you suggest). In fact, the way you do it now with ValueObject's and ids is already entity like.

  • Has an ID (a uuid) - technically other aggregates or bounded domains could reference this, but this is not encouraged.

Agreed.

  • Is instantiated with id, attributes.

Agreed

  • Has no events or history.

Yes the AggregateRoot takes care of this.

  • Is equal to another Entity if both the ID and its attributes are equal.
  • Is immutable.

I think this should be only on id. Consider the seat example in DDD. Each seat in a plane on a flight is defined by a unique number. If we would replace the coating of the seat it would still be the same seat (meaning it would keep the same number). So I think it is mutable, you can change it attributes, but not the id.

  • Is included when marshalling for snapshots.

Agreed. With our current marshal approach this will happen out of the box

  • cannot be used as type in commands and events.

Agreed. It is good add some concrete example on how to use Entities in the documentation. We could also check and fail if this happens to ensure this is not happening.

  • has a replace() method for ergonomic updates to attributes.

We already have Sequent::Core::Helpers::Copyable for this.

  • Has no other behaviour and is side-effect-free.

Agreed

And if a PR is welcome for this, what would you want included? Should I e.g. include it in the documentation/manual; iirc the value-objects are not in there either.

Yes a PR is welcome. It should include specs, and comply to our rubocop config. It would be great if you could it in the documentation as well. The ValueObject is in the docs btw, see https://www.sequent.io/docs/concepts/value-object.html

lvonk avatar Dec 29 '21 17:12 lvonk

Thanks for the detailed reply.

Either way I don't think, or see how, that will change what Sequent has as an Aggregate significantly. It is, to me, an implementation detail.

Sorry, I should have been more clear. What I meant was a situation in which an Aggregate has no identity at all, in which an aggregate holds one RootEntity and other Entities. Where that aggregate is really little more than a wrapper around all this.

Pseudocode:

class BlogAggregate < Sequent::Core::AggregateRoot
    root_entity_class BlogPost
end

id = BlogAggregate.new(events).root_entity.id

That might be closer to what Evans describes in his DDD book, but I doubt it offers benefits for Sequent; and it is rather different from what Sequent has now: where the aggregate is a root entity, not has a root entity.

Yes a PR is welcome.

I'll whip up a first PR and obviously will add the specs (I work B/TDD so it comes naturally for me ;)).

WRT timeline: I'm in need of this myself, but am also closing in on some deadlines, so extracting, polising and making the PR is not top-prio for me this & next week. But I'll be working on this between the lines and in the second half of Jan.

berkes avatar Dec 30 '21 18:12 berkes

But I'll be working on this between the lines and in the second half of Jan.

Sure, no rush. Very happy that you want to contribute this 🙌🏼

What I meant was a situation in which an Aggregate has no identity at all, in which an aggregate holds one RootEntity and other Entities

and is rather different from what Sequent has now: where the aggregate is a root entity, not has a root entity.

Okay, this made me reread some DDD articles and discussions again. But as far as I understand it AggregateRoot and RootEntity mean the same in DDD. They control all access and state changes to all the objects within that aggregate. I think my confusion was because of the term aggregate here.

Fowler describes an aggregate as follows: "Aggregate is a pattern in Domain-Driven Design. A DDD aggregate is a cluster of domain objects that can be treated as a single unit." and "An aggregate will have one of its component objects be the aggregate root.".

Since aggregate is a pattern, in Sequent, the aggregate is not something concrete in the sense that you can extend or include it or something. In Sequent the aggregate typically is a folder containing the cluster of objects that make-up the aggregate. That folder will at least have an AggregateRoot. Although nothing prevents you in Sequent to structure your code differently.

lvonk avatar Dec 31 '21 13:12 lvonk

Closing this issue for now due to inactivity

lvonk avatar May 24 '23 07:05 lvonk