AggregateRoot.java

package org.sanmibuh.ddd.domain;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public abstract class AggregateRoot<I extends AggregateRootId<?>> {

  private final I id;
  private final List<DomainEvent> domainEvents = new ArrayList<>();

  protected AggregateRoot(final I id) {
    this.id = id;
  }

  public final I id() {
    return id;
  }

  protected final void recordEvent(final DomainEvent event) {
    domainEvents.add(event);
  }

  public final List<DomainEvent> domainEvents() {
    return Collections.unmodifiableList(domainEvents);
  }

  // Entity identity: equal iff same concrete type and same id, ignoring state and recorded events.
  // Concrete aggregates are final (enforced by ArchUnit), so a distinct class is always a distinct
  // aggregate; getClass() captures that precisely, whereas instanceof AggregateRoot would make two
  // aggregate types sharing an id compare equal.
  // Hand-written rather than Lombok on purpose: identity is defined once here, in the base, so
  // every aggregate inherits it for free and it cannot be silently forgotten on a new one.
  // Lombok's instanceof/canEqual would only discriminate by concrete type if
  // @EqualsAndHashCode(callSuper = true) were repeated on every leaf.
  @Override
  @SuppressWarnings("EqualsGetClass")
  public final boolean equals(final Object o) {
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    return id.equals(((AggregateRoot<?>) o).id);
  }

  @Override
  public final int hashCode() {
    return id.hashCode();
  }
}