Abstract

This document presents a comprehensive guide aimed at facilitating the implementation of Domain Driven Design (DDD) by integrating the faggregate library, which harmoniously combines the concepts of Aggregates with functional programming paradigms. The library is designed to offer developers an efficient means to navigate and simplify intricate development challenges. Through practical exercises and detailed explanations, this tutorial aims to equip readers with the necessary skills and knowledge to adeptly apply the library’s functionalities.

Introduction

faggregate is a Java library helping to define Aggregate according to the Domain Driven Design approach with a touch of Functional Programming. The source code of faggregate is available on github.com/tmorin/faggregate.

faggregate synthesizes the robust methodologies of DDD Aggregates and functional programming to streamline the software development process.

DDD Aggregates offer a structured approach to managing complex domain entities and ensuring transactional consistency. In parallel, functional programming emphasizes a declarative coding style, enhancing code clarity and simplifying maintenance efforts. The amalgamation of these methodologies not only fosters more stable and reliable software but also minimizes the incidence of defects.

Furthermore, the emphasis on functional programming augments code modularity and facilitates straightforward refactoring, attributes highly beneficial in the context of extensive software development endeavors.

In essence, faggregate empowers developers to create systems that are both scalable and maintainable, adeptly addressing the needs of their specific business domains.

The scope

The purpose of this tutorial is to build from scratch the Counter aggregate. As its name suggests, the Counter aggregate is capable of tracking a value that is incremented. The counter is incremented when the CounterIncremented domain event is produced. The event is produced when the IncrementCounter command is invoked.

es increment
Figure 1. Modeling of the IncrementCounter command

In this tutorial, we will cover a series of foundational steps necessary for building a robust application using the framework. This journey will begin with bootstrapping a Maven project, setting the groundwork for our development process. We will then delve into defining the Counter aggregate, a core component of our application’s domain model. Following this, we’ll tackle implementing the command handler for the IncrementCounter command, allowing us to interact with our aggregate. We’ll also implement the mutator for the CounterIncremented domain event to handle state changes. The creation of the repository for the Counter aggregate will be our next step, ensuring we have a solid persistence mechanism. We’ll continue with implementing the configurer of the Counter aggregate, tying all components together for a cohesive application configuration. Finally, we’ll explore the validation of the Counter aggregate, ensuring that our implementation meets the requirements of our business domain.

The source code of the tutorial is available on github.com/tmorin/faggregate.

The Maven module

The Maven module brings four dependencies. The first one, faggregate-core-simple, brings the faggregate library. The second one, faggregate-core-scenario, brings an extension of faggregate library to implement acceptance tests. The third one, junit-jupiter, provides a test framework. Finally, lombok, spices up java helping to save lines of codes.

./pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>io.morin.faggregate</groupId>
    <artifactId>examples-tutorial</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <description>Elevate your DDD implementation with this Tutorial on a Java library that blends Aggregates and functional programming.</description>
    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>
    <dependencies>
        <!-- LOCAL -->
        <dependency>
            <artifactId>faggregate-core-simple</artifactId>
            <groupId>io.morin.faggregate</groupId>
            <version>1.5.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <artifactId>faggregate-core-scenario</artifactId>
            <groupId>io.morin.faggregate</groupId>
            <version>1.5.0-SNAPSHOT</version>
        </dependency>
        <!-- TEST -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.9.2</version>
            <scope>test</scope>
        </dependency>
        <!-- COMPILATION -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.26</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>

The Aggregate

The implementation of the Aggregate starts with the structural elements:

  • Entity: it’s an object within the system that has a unique identity and attributes that describe its state. An entity’s identity is typically represented by an ID or a key, and its state changes over time as it participates in the system’s interactions.

  • Command: it’s a request for the system to perform a specific action. Commands are often used to initiate changes in the state of the system.

  • Domain Event: it’s a record of something that has occurred within the system. Domain events are used to capture the state changes that occur as a result of processing commands.

The Root Aggregate

A Root Aggregate is a cluster of related entities that are treated as a single unit with respect to transactions and consistency. The root aggregate defines a boundary around a set of related entities, and it is responsible for ensuring that changes to its entities are made in a consistent and valid state.

In this tutorial, the Root Aggregate of the Counter Aggregate, is the Counter Entity. Usually, a Root Aggregate is an Entity which is mutable. However, faggregate emphasizes a functional approach, therefore the Root Aggregate is an immutable structure.

src/main/java/faggregate/tutorial/Counter.java
package faggregate.tutorial;

import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.Value;

/**
 * {@link Counter} is an Entity which is also the Root Aggregate of the Counter Aggregate.
 * <p>
 * The class is <i>immutable</i> because of the usage of {@link Value} Lombok annotation.
 */
@Value
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class Counter {

    /**
     * The default value.
     */
    static final int DEFAULT_VALUE = 0;
    /**
     * The identifier of the counter.
     */
    String counterId;
    /**
     * The current state of the counter.
     */
    int value;

    /**
     * The factory should be used to initialize a new aggregate.
     *
     * @param counterId the identifier
     * @return the new aggregate
     */
    static Counter create(String counterId) {
        return new Counter(counterId, DEFAULT_VALUE);
    }

    /**
     * Update the value of the counter.
     *
     * @param newValue the new value
     * @return a new instance
     */
    Counter updateValue(int newValue) {
        return new Counter(counterId, newValue);
    }
}

The Root Aggregate is covered by the following unit test.

src/test/java/faggregate/tutorial/CounterTest.java
package faggregate.tutorial;

import static org.junit.jupiter.api.Assertions.assertEquals;

import lombok.val;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class CounterTest {

    static String DEFAULT_COUNTER_ID = "counter";
    Counter counter;

    @BeforeEach
    void setUp() {
        counter = Counter.create(DEFAULT_COUNTER_ID);
    }

    @Test
    void shouldHaveDefaultFields() {
        assertEquals(DEFAULT_COUNTER_ID, counter.getCounterId());
        assertEquals(0, counter.getValue());
    }

    @Test
    void shouldUpdateValue() {
        val newValue = 3;
        val newCounter = counter.updateValue(newValue);
        assertEquals(newValue, newCounter.getValue());
    }
}

The Command

The IncrementCounter Command transfers the intention to increment the counter.

src/main/java/faggregate/tutorial/IncrementCounter.java
package faggregate.tutorial;

import lombok.Value;

/**
 * The command to increment a counter.
 */
@Value
public class IncrementCounter {

    /**
     * The identifier of the counter.
     */
    String counterId;
}

The Domain Event

The CounterChanged Domain Event states the value of a counter has been changed.

src/main/java/faggregate/tutorial/CounterChanged.java
package faggregate.tutorial;

import lombok.Value;

/**
 * The event states the value of a counter changed.
 */
@Value
public class CounterChanged {

    /**
     * The identifier of the counter.
     */
    String counterId;

    /**
     * The value before the change.
     */
    int oldValue;

    /**
     * The value after the change.
     */
    int newValue;
}

The Command Handler

A Command Handler is a pure function which executes a Command according to the latest known state of an Aggregate. A Command Handler returns a Result which will be sent back to the initiator of the command as well as a set of Domain Events explaining the outcome of the Command execution.

obj command handler
Figure 2. Input and output of a command handler

The IncrementCounterHandler Command Handler handles the IncrementCounter Command and produces the CounterChanged Domain Event based on the latest known state of the Counter Aggregate.

src/main/java/faggregate/tutorial/IncrementCounterHandler.java
package faggregate.tutorial;

import io.morin.faggregate.api.Handler;
import io.morin.faggregate.api.Output;
import io.morin.faggregate.api.OutputBuilder;
import java.util.concurrent.CompletableFuture;
import lombok.val;

/**
 * The {@link IncrementCounterHandler} Command Handler handles the {@link IncrementCounter} Command and produces
 * the {@link CounterChanged} Domain Event based on the latest known state of the {@link Counter} Aggregate.
 */
class IncrementCounterHandler implements Handler<Counter, IncrementCounter, Void> {

    /**
     * There is no special business logic leading to the publication of the {@link CounterChanged} domain event.
     * Therefore, the implementation of the handle method is straightforward.
     *
     * @param state   the current state of the aggregate
     * @param command the command
     * @return the handling output
     */
    @Override
    public CompletableFuture<Output<Void>> handle(Counter state, IncrementCounter command) {
        val oldValue = state.getValue();
        val newValue = oldValue + 1;
        val counterChanged = new CounterChanged(state.getCounterId(), oldValue, newValue);
        return CompletableFuture.completedFuture(OutputBuilder.get().add(counterChanged).build());
    }
}

The Command Handler is covered by the following unit test.

src/test/java/faggregate/tutorial/IncrementCounterHandlerTest.java
package faggregate.tutorial;

import static org.junit.jupiter.api.Assertions.assertEquals;

import lombok.SneakyThrows;
import lombok.val;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class IncrementCounterHandlerTest {

    static String DEFAULT_COUNTER_ID = "counter";
    Counter counter;
    IncrementCounter incrementCounter;
    IncrementCounterHandler incrementCounterHandler;

    @BeforeEach
    void setUp() {
        counter = Counter.create(DEFAULT_COUNTER_ID);
        incrementCounter = new IncrementCounter(counter.getCounterId());
        incrementCounterHandler = new IncrementCounterHandler();
    }

    @Test
    @SneakyThrows
    void shouldProduceCounterChanged() {
        val expected = new CounterChanged(DEFAULT_COUNTER_ID, 0, 1);
        val output = incrementCounterHandler.handle(counter, incrementCounter).get();
        assertEquals(expected, output.getEvents().get(0));
    }
}

The Mutator

A Mutator is a pure function which mutates the state of an Aggregate according to a Domain Event. A Mutator returns a new instance of the Aggregate state.

obj mutator
Figure 3. Input and output of a mutator

The CounterChangedMutator Mutator mutates the state of the Counter Aggregate according to the CounterChangedMutator Domain Event.

src/main/java/faggregate/tutorial/CounterChangedMutator.java
package faggregate.tutorial;

import io.morin.faggregate.api.Mutator;

/**
 * The ${@link CounterChangedMutator} Mutator mutates the state of the {@link Counter} Aggregate according to
 * the {@link CounterChangedMutator} Domain Event.
 */
class CounterChangedMutator implements Mutator<Counter, CounterChanged> {

    /**
     * Mutate the aggregate and return a new state.
     *
     * @param state the initial state
     * @param event the event
     * @return the mutated state
     */
    @Override
    public Counter mutate(Counter state, CounterChanged event) {
        return state.updateValue(event.getNewValue());
    }
}

The Mutator is covered by the following unit test.

src/test/java/faggregate/tutorial/CounterChangedMutatorTest.java
package faggregate.tutorial;

import static org.junit.jupiter.api.Assertions.assertEquals;

import lombok.val;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class CounterChangedMutatorTest {

    static String DEFAULT_COUNTER_ID = "counter";
    Counter counter;
    CounterChangedMutator counterChangedMutator;

    @BeforeEach
    void setUp() {
        counter = Counter.create(DEFAULT_COUNTER_ID);
        counterChangedMutator = new CounterChangedMutator();
    }

    @Test
    void shouldMutateValue() {
        val counterChanged = new CounterChanged(DEFAULT_COUNTER_ID, 0, 1);
        val newState = counterChangedMutator.mutate(counter, counterChanged);
        assertEquals(counterChanged.getNewValue(), newState.getValue());
    }
}

The Repository

A repository is a set of functions which initiates, loads, persists and destroys the state of an Aggregate.

Side effects

In faggregate, we define those functions as side effects:

  • initializer: it initializes the state of an Aggregate

  • loader: it loads the latest known state of an Aggregate

  • persister: it persists the latest known state and/or the generated Domain Events of an Aggregate

  • destroyer: it destroys the persisted data related to an Aggregate

The side effects are applied according to the intention of the handled command. For instance, the initializer is executed when a Command Handler has been registered with the INITIALIZATION intention. The details of the intentions are explained in the following sections.

The default implementation of faggregate, i.e. faggregate-core-simple, provides default implementation throwing an UnsupportedOperationException. Therefore, according to your context, you have to implement and register the side effects.

The Initializer

An initializer is executed when the type of the handled Command has been registered with the INITIALIZATION intention.

obj initializer
Figure 4. Input and output of an initializer

In this tutorial, the initializer is not implemented because there is no command handled with the INITIALIZATION intention.

The Loader

A loader is executed when the type of the handled Command has been registered with the MUTATION intention.

obj loader
Figure 5. Input and output of a loader

In this tutorial, the loader is implemented because the IncrementCounter command is handled with the MUTATION intention.

The Persister

A persister is executed when the type of the handled Command has been registered with the INITIALIZATION or MUTATION intentions.

obj persister
Figure 6. Input and output of a persister

In this tutorial, the persister is implemented because the IncrementCounter command is handled with the MUTATION intention.

The Destroyer

A destroyer is executed when the type of the handled command has been registered with the DESTRUCTION intention.

obj destroyer
Figure 7. Input and output of a destroyer

In this tutorial, the destroyer is not implemented because there is no command handled with the DESTRUCTION intention.

The implementation

src/main/java/faggregate/tutorial/CounterRepository.java
package faggregate.tutorial;

import io.morin.faggregate.api.Context;
import io.morin.faggregate.api.Loader;
import io.morin.faggregate.api.Persister;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import lombok.RequiredArgsConstructor;
import lombok.val;

/**
 * An in memory implementation of both side effects Loader and Persister.
 */
@RequiredArgsConstructor
class CounterRepository implements Loader<String, Counter>, Persister<String, Counter> {

    /**
     * The map stores the states of the aggregates.
     */
    final Map<String, Counter> statesByIdentifier;

    /**
     * The mao stores the domain events of the aggregates.
     */
    final Map<String, List<Object>> eventsByIdentifier;

    static CounterRepository create() {
        return new CounterRepository(new HashMap<>(), new HashMap<>());
    }

    @Override
    public CompletableFuture<Optional<Counter>> load(Context<String, ?> context) {
        val counter = statesByIdentifier.getOrDefault(context.getIdentifier(), Counter.create(context.getIdentifier()));
        return CompletableFuture.completedFuture(Optional.of(counter));
    }

    @Override
    public <E> CompletableFuture<Void> persist(Context<String, ?> context, Counter state, List<E> events) {
        statesByIdentifier.put(context.getIdentifier(), state);
        if (!eventsByIdentifier.containsKey(context.getIdentifier())) {
            eventsByIdentifier.put(context.getIdentifier(), new ArrayList<>());
        }
        eventsByIdentifier.get(context.getIdentifier()).addAll(events);
        return CompletableFuture.completedFuture(null);
    }
}

The Repository is covered by the following unit test.

src/test/java/faggregate/tutorial/CounterRepositoryTest.java
package faggregate.tutorial;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;

import io.morin.faggregate.simple.core.ExecutionContext;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import lombok.SneakyThrows;
import lombok.val;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class CounterRepositoryTest {

    Map<String, Counter> states;
    Map<String, List<Object>> events;
    CounterRepository counterRepository;

    @BeforeEach
    void setUp() {
        states = new HashMap<>();
        events = new HashMap<>();
        counterRepository = new CounterRepository(states, events);
    }

    @Test
    @SneakyThrows
    void shouldLoadWhenUnknown() {
        val identifier = UUID.randomUUID().toString();
        val optionState = counterRepository.load(ExecutionContext.create(identifier, "command")).get();
        assertFalse(optionState.isEmpty());
        assertEquals(identifier, optionState.get().getCounterId());
    }

    @Test
    @SneakyThrows
    void shouldLoadWhenKnown() {
        val identifier = UUID.randomUUID().toString();
        val counter = Counter.create(identifier);
        states.put(identifier, counter);
        val context = ExecutionContext.create(identifier, "command");
        val optionState = counterRepository.load(context).get();
        assertFalse(optionState.isEmpty());
        assertEquals(counter, optionState.get());
    }

    @Test
    @SneakyThrows
    void shouldPersist() {
        val identifier = UUID.randomUUID().toString();
        val counter = Counter.create(identifier);
        val event = "event";
        val context = ExecutionContext.create(identifier, "command");
        counterRepository.persist(context, counter, List.of(event)).get();
        assertEquals(states.get(identifier), counter);
        assertEquals(events.get(identifier).get(0), event);
    }
}

The Configurer

A Configurer binds the handlers, mutators and all side effects of an Aggregate. For ease, there is only one Configurer of the Counter Aggregate. In fact, in production grade project, there is Configured for the core domain and also ones in infrastructure, e.g. one for a relational database, another one for an in memory storage …​.

src/main/java/faggregate/tutorial/CounterConfigurer.java
package faggregate.tutorial;

import io.morin.faggregate.api.AggregateManagerBuilder;
import io.morin.faggregate.api.Configurer;
import io.morin.faggregate.api.Loader;
import io.morin.faggregate.api.Persister;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;

/**
 * The configurer binds the handlers, mutator as well as side effects.
 */
@RequiredArgsConstructor
public class CounterConfigurer implements Configurer<String, Counter> {

    private final CounterRepository repository;

    /**
     * Create a new instance of the configurer.
     *
     * @param repository the repository to use
     * @return the configurer
     */
    public static CounterConfigurer create(@NonNull CounterRepository repository) {
        return new CounterConfigurer(repository);
    }

    @Override
    public void configure(AggregateManagerBuilder<String, Counter> builder) {
        // set the loader
        builder.set((Loader<String, Counter>) repository);
        // set the persister
        builder.set((Persister<String, Counter>) repository);
        // add the command handler
        builder.add(IncrementCounter.class, new IncrementCounterHandler());
        // add the mutator
        builder.add(CounterChanged.class, new CounterChangedMutator());
    }
}

The Configurer is covered by the following unit test.

src/test/java/faggregate/tutorial/CounterConfigurerTest.java
package faggregate.tutorial;

import static org.junit.jupiter.api.Assertions.assertEquals;

import io.morin.faggregate.api.AggregateManager;
import io.morin.faggregate.api.AggregateManagerBuilder;
import io.morin.faggregate.simple.core.SimpleAggregateManagerBuilder;
import lombok.SneakyThrows;
import lombok.val;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class CounterConfigurerTest {

    CounterRepository counterRepository;
    AggregateManager<String> counterManager;

    @BeforeEach
    void setUp() {
        final AggregateManagerBuilder<String, Counter> counterManagerBuilder = SimpleAggregateManagerBuilder.get();
        counterRepository = CounterRepository.create();
        val counterConfigurer = new CounterConfigurer(counterRepository);
        counterConfigurer.configure(counterManagerBuilder);
        counterManager = counterManagerBuilder.build();
    }

    @Test
    @SneakyThrows
    void shouldConfigure() {
        val counterId = "counter-0";
        counterManager.execute(counterId, new IncrementCounter(counterId)).get();
        assertEquals(1, counterRepository.statesByIdentifier.size());
    }
}

The validation

The building blocks offered by faggregate facilitate the separation of the API, core, and side effects layers. This architecture allows the core to define acceptance scenarios with commands, events, and states, essential for validating the implementations of side effects. Consequently, side effects are testable from the core’s perspective, which is crucial for ensuring that business rules are correctly implemented and accepted. This design principle underpins the framework’s approach to robust, domain-driven application development.

In this tutorial, we explore two key scenarios using faggregate: the creation and the incrementation of a counter, both of which are organized within the same suite. In the context of faggregate, a suite refers to a collection of acceptance scenarios.

A scenario follows the Given-When-Then pattern, which is a widely used format for writing acceptance tests. It consists of three parts:

  • Given: the initial state of the system

  • When: the action that triggers the change in the system

  • Then: the expected outcome of the action

The acceptance suite of the Counter aggregate is provided by the following factory.

src/main/java/faggregate/tutorial/CounterSuiteFactory.java
package faggregate.tutorial;

import io.morin.faggregate.core.validation.Scenario;
import io.morin.faggregate.core.validation.Suite;
import lombok.experimental.UtilityClass;
import lombok.val;

/**
 * The {@link CounterSuiteFactory} class is a factory class that creates a suite of scenarios to validate the
 * behavior of the {@link Counter} Aggregate.
 */
@UtilityClass
public class CounterSuiteFactory {

    /**
     * Creates a suite of scenarios to validate the behavior of the {@link Counter} Aggregate.
     *
     * @return the suite of scenarios
     */
    public Suite create() {
        // Define a scenario validating the creation of a counter
        val scenarioAId = "counter#a";
        val scenarioA = Scenario
            .builder()
            .name("Counter Creation")
            // no state initialization is required for the Counter Creation scenario
            .given(Scenario.Given.builder().identifier(scenarioAId).build())
            // trigger the increment of the counter
            .when(Scenario.When.builder().command(new IncrementCounter(scenarioAId)).build())
            .then(
                Scenario.Then
                    .builder()
                    // validate the state of the aggregate has been incremented
                    .state(Counter.create(scenarioAId).updateValue(1))
                    // validate the right event has been published
                    .event(new CounterChanged(scenarioAId, 0, 1))
                    .build()
            )
            .build();

        // Define a scenario validating the increment of a counter
        val scenarioBId = "counter#b";
        val scenarioB = Scenario
            .builder()
            .name("Counter Creation")
            // initialize the state of the aggregate with a value of 1 for the Counter Increment scenario
            .given(Scenario.Given.builder().identifier(scenarioBId).command(new IncrementCounter(scenarioBId)).build())
            // trigger the increment of the counter
            .when(Scenario.When.builder().command(new IncrementCounter(scenarioBId)).build())
            .then(
                Scenario.Then
                    .builder()
                    // validate the state of the aggregate has been incremented
                    .state(Counter.create(scenarioBId).updateValue(2))
                    // validate the right event has been published
                    .event(new CounterChanged(scenarioBId, 1, 2))
                    .build()
            )
            .build();

        // Create and return a suite containing the two scenarios
        return Suite.builder().scenario(scenarioA).scenario(scenarioB).build();
    }
}

The execution of the acceptance suite is handled by a usual JUnit test.

src/main/java/faggregate/tutorial/CounterSuiteFactory.java
package faggregate.tutorial;

import io.morin.faggregate.simple.core.SimpleAggregateManagerBuilder;
import java.util.concurrent.CompletableFuture;
import lombok.val;
import org.junit.jupiter.api.Test;

/**
 * This JUnit test validates the side effect implementation executing the CounterSuite.
 */
class CounterSuiteTest {

    @Test
    void shouldValidateSideEffects() {
        // create the repository
        val counterRepository = CounterRepository.create();

        // create the builder for the aggregate manager
        val counterManagerBuilder = SimpleAggregateManagerBuilder.<String, Counter>get();

        // create the configurer and configure the aggregate manager
        // i.e. register the handlers, mutators and side effects
        CounterConfigurer.create(counterRepository).configure(counterManagerBuilder);

        // build the aggregate manager
        val counterManager = counterManagerBuilder.build();

        // execute the suite providing the aggregate manager
        CounterSuiteFactory
            .create()
            .execute(
                // the aggregate manager to test
                counterManager,
                // an optional lambda to store the initialize state of the aggregate before the _Given_ phase
                // this functionality is not required for the CounterSuite, so the implementation is null
                null,
                // an optional lambda to load the state of the aggregate before the _Then_ phase
                identifier -> CompletableFuture.completedFuture(counterRepository.statesByIdentifier.get(identifier))
            );
    }
}

Conclusion

In conclusion, this tutorial offers a comprehensive exploration of the faggregate Java library, a potent resource for developers aiming to integrate Domain Driven Design Aggregates with functional programming principles. Through the meticulous construction of the Counter aggregate example, it showcases how faggregate simplifies the development of complex, domain-centric software systems, promoting maintainability and scalability.

The step-by-step guidance provided throughout the tutorial, from setting up a Maven project to implementing domain-specific logic and persistence mechanisms, equips developers with the tools and knowledge needed to fully harness the capabilities of faggregate. This approach not only facilitates the building of high-quality software that aligns with business requirements but also enhances the overall development experience by leveraging functional programming’s strengths in clarity, modularity, and immutability.

As a valuable educational resource, the tutorial underscores the significance of adopting DDD Aggregates and functional programming in modern software development. It empowers developers to create software systems that are not only robust and scalable but also aligned with the strategic goals of their business domains. Thus, for those seeking to deepen their understanding of DDD and functional programming or to elevate the quality of their software projects, this tutorial proves to be an indispensable guide.