1

This is an overly simplistic example of a domain model for a time keeping domain. I only started on it today as a way to practice the techniques I'm learning while reading Implementing Domain-Driven-Design by Vaughn Vernon.

I am using the Axon framework to model my Aggregate (posted below).

One big question I had in my Design was the TimeCardEntry objects. Is it appropriate for me to keep a running list of Entries? Should they contain an ID at all or even be an AggregateMember?

I am still working through the details, but I imagine eventually I will need to add a query layer that will allow viewing a timecard's data.

I will also likely eventually need an ability to udpate existing time card entries via an UpdateTimeCardEntry command.

@Slf4j
@Aggregate
public class TimeCard {

  @AggregateIdentifier
  private String employeeName;

  @AggregateMember(eventForwardingMode = ForwardMatchingInstances.class)
  private List<TimeCardEntry> timeCardEntries = new ArrayList<>();

  public TimeCard() {
    //Empty Constructor for Axon framework
  }

  @CommandHandler
  public TimeCard(ClockInCommand cmd) {
    apply(new ClockInEvent(cmd.getEmployeeName(),
        GenericEventMessage.clock.instant(),
        cmd.getTimeCardEntryId()));
  }

  @CommandHandler
  public void handle(ClockOutCommand cmd) {
    getEntryIfOpen(cmd.getTimeCardEntryId()).
        ifPresentOrElse(
            entry -> apply(
                new ClockOutEvent(cmd.getEmployeeName(),
                    GenericEventMessage.clock.instant(),
                    entry.timeCardEntryId)),
            () -> log.error("Employee has not clocked in or is already clocked out"));
  }

  @EventSourcingHandler
  public void on(ClockInEvent event) {
    this.employeeName = event.getEmployeeName();
    timeCardEntries.add(new TimeCardEntry(event.getTimeCardEntryId(), event.getTime()));
  }

  private Optional<TimeCardEntry> getEntryIfOpen(String timeCardEntryId) {
    return timeCardEntries.stream()
        .filter(entry -> entry.getTimeCardEntryId().equals(timeCardEntryId))
        .filter(TimeCardEntry::isClockedIn)
        .findFirst();
  }

  @Data
  public class TimeCardEntry {

    @EntityId
    private final String timeCardEntryId;
    private final Instant clockInTime;
    private Instant clockOutTime;

    @EventSourcingHandler
    public void on(ClockOutEvent event) {
      this.clockOutTime = event.getTime();
    }

    private boolean isClockedIn() {
      return clockOutTime == null;
    }
  }
}

I have the following tests -

class TimeCardTest {

  AggregateTestFixture<TimeCard> testFixture;

  @BeforeEach
  void setUp() {
    testFixture = new AggregateTestFixture<>(TimeCard.class);
  }

  @ParameterizedTest
  @MethodSource(value = "randomEmployeeName")
  void testClockInCommand(String employeeName) {
    String randomUUID = UUID.randomUUID().toString();
    testFixture.givenNoPriorActivity()
        .when(new ClockInCommand(employeeName, randomUUID))
        .expectEvents(new ClockInEvent(employeeName, testFixture.currentTime(), randomUUID));
  }

  @ParameterizedTest
  @MethodSource(value = "randomEmployeeName")
  void testClockOutCommand(String employeeName) {
    String randomUUID = UUID.randomUUID().toString();
    testFixture
        .givenCommands(new ClockInCommand(employeeName, randomUUID))
        .when(new ClockOutCommand(employeeName, randomUUID))
        .expectEvents(new ClockOutEvent(employeeName, testFixture.currentTime(), randomUUID));
  }

  private static Stream<String> randomEmployeeName() {
    return Stream.generate(RandomString::make).limit(10);
  }
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.