diff --git a/README.md b/README.md index f73aa6e..436a159 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # CQRS and event sourcing app [![Build Status](https://travis-ci.org/daggerok/cqrs-eventsourcing-user-management-example.svg?branch=master)](https://travis-ci.org/daggerok/cqrs-eventsourcing-user-management-example) CQRS and event sourcing using dynamic groovy, spring-boot and spring-webflux +Status: _in progress, implemented in-memory event store only, follow updates..._ + ```bash ./gradlew bootRun diff --git a/src/main/groovy/com/github/daggerok/eventsourcing/user/InMemoryUserRepository.java b/src/main/groovy/com/github/daggerok/eventsourcing/user/InMemoryUserRepository.java index d826afd..4e29e5d 100644 --- a/src/main/groovy/com/github/daggerok/eventsourcing/user/InMemoryUserRepository.java +++ b/src/main/groovy/com/github/daggerok/eventsourcing/user/InMemoryUserRepository.java @@ -25,9 +25,8 @@ public class InMemoryUserRepository implements UserRepository { @Override public User find(UUID userId) { - User snapshot = new User(userId); return eventStore.containsKey(userId) - ? User.recreate(snapshot, eventStore.get(userId)) - : snapshot; + ? User.recreate(userId, eventStore.get(userId)) + : null; } } diff --git a/src/main/groovy/com/github/daggerok/eventsourcing/user/User.java b/src/main/groovy/com/github/daggerok/eventsourcing/user/User.java index 4da66f9..b5ce6b1 100644 --- a/src/main/groovy/com/github/daggerok/eventsourcing/user/User.java +++ b/src/main/groovy/com/github/daggerok/eventsourcing/user/User.java @@ -2,12 +2,11 @@ package com.github.daggerok.eventsourcing.user; import com.github.daggerok.eventsourcing.user.event.DomainEvent; import com.github.daggerok.eventsourcing.user.event.UserActivated; +import com.github.daggerok.eventsourcing.user.event.UserCreated; import com.github.daggerok.eventsourcing.user.event.UserDeactivated; import io.vavr.API; -import lombok.Getter; -import lombok.ToString; +import lombok.*; -import java.time.ZonedDateTime; import java.util.Collection; import java.util.UUID; import java.util.concurrent.CopyOnWriteArrayList; @@ -21,12 +20,12 @@ import static io.vavr.Predicates.instanceOf; * Created user can be: * - activated * - deactivated - * + *

* Activated user can be: * - deactivated * Activated user cannot be: * - activated - * + *

* Deactivated user can be: * - activated * Deactivated user cannot be: @@ -34,27 +33,45 @@ import static io.vavr.Predicates.instanceOf; */ @Getter @ToString +@NoArgsConstructor public class User implements Function { - private final Collection eventStream = new CopyOnWriteArrayList<>(); - private final UUID userId; + private UUID userId; private UserStatus state; - public User(UUID userId) { - this.userId = userId; - state = UserStatus.PENDING; - } + private final Collection eventStream = new CopyOnWriteArrayList<>(); public void flushEvents() { eventStream.clear(); } - // cmd 1: + public User(UUID userId) { + create(userId); + } + public void create(UUID userId) { + onCreate(new UserCreated(userId)); + } + + // cmd 0: create + public void create() { + create(UUID.randomUUID()); + } + + // evt: 0 + private User onCreate(UserCreated event) { + eventStream.add(event); + this.userId = event.getAggregateId(); + state = UserStatus.PENDING; + return this; + } + + // cmd 1: activate public void activate() { if (state == UserStatus.ACTIVE) throw new IllegalStateException("user is already active"); - onActivate(new UserActivated(userId, ZonedDateTime.now())); + onActivate(new UserActivated(userId)); } + // evt: 1 private User onActivate(UserActivated event) { eventStream.add(event); @@ -62,13 +79,14 @@ public class User implements Function { return this; } - // cmd 2: + // cmd 2: deactivate public void deactivate() { if (state == UserStatus.SUSPENDED) throw new IllegalStateException("user is already suspended"); - onDeactivate(new UserDeactivated(userId, ZonedDateTime.now())); + onDeactivate(new UserDeactivated(userId)); } - // evt 2: + + // evt: 2 private User onDeactivate(UserDeactivated event) { eventStream.add(event); state = UserStatus.SUSPENDED; @@ -77,14 +95,16 @@ public class User implements Function { /* es */ - public static User recreate(User snapshot, Collection domainEvents) { + public static User recreate(UUID userId, Collection domainEvents) { + User snapshot = new User(userId); return io.vavr.collection.List.ofAll(domainEvents) - .foldLeft(snapshot, User::apply); + .foldLeft(snapshot, User::apply); } @Override public User apply(DomainEvent domainEvent) { return API.Match(domainEvent).of( + Case($(instanceOf(UserCreated.class)), this::onCreate), Case($(instanceOf(UserActivated.class)), this::onActivate), Case($(instanceOf(UserDeactivated.class)), this::onDeactivate) ); diff --git a/src/main/groovy/com/github/daggerok/eventsourcing/user/event/UserActivated.java b/src/main/groovy/com/github/daggerok/eventsourcing/user/event/UserActivated.java index 00ecf47..e0f6317 100644 --- a/src/main/groovy/com/github/daggerok/eventsourcing/user/event/UserActivated.java +++ b/src/main/groovy/com/github/daggerok/eventsourcing/user/event/UserActivated.java @@ -1,15 +1,19 @@ package com.github.daggerok.eventsourcing.user.event; -import lombok.*; +import com.github.daggerok.eventsourcing.user.UserStatus; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.ToString; import java.time.ZonedDateTime; import java.util.UUID; @Getter @ToString -@AllArgsConstructor -@NoArgsConstructor(access = AccessLevel.PACKAGE) +@RequiredArgsConstructor public class UserActivated implements DomainEvent { - UUID aggregateId; - ZonedDateTime at; + @NonNull UUID aggregateId; + final UserStatus state = UserStatus.ACTIVE; + final ZonedDateTime at = ZonedDateTime.now(); } diff --git a/src/main/groovy/com/github/daggerok/eventsourcing/user/event/UserCreated.java b/src/main/groovy/com/github/daggerok/eventsourcing/user/event/UserCreated.java new file mode 100644 index 0000000..b945cf1 --- /dev/null +++ b/src/main/groovy/com/github/daggerok/eventsourcing/user/event/UserCreated.java @@ -0,0 +1,19 @@ +package com.github.daggerok.eventsourcing.user.event; + +import com.github.daggerok.eventsourcing.user.UserStatus; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +import java.time.ZonedDateTime; +import java.util.UUID; + +@Getter +@ToString +@RequiredArgsConstructor +public class UserCreated implements DomainEvent { + @NonNull UUID aggregateId; + final UserStatus state = UserStatus.PENDING; + final ZonedDateTime at = ZonedDateTime.now(); +} diff --git a/src/main/groovy/com/github/daggerok/eventsourcing/user/event/UserDeactivated.java b/src/main/groovy/com/github/daggerok/eventsourcing/user/event/UserDeactivated.java index 19c3517..afa38cc 100644 --- a/src/main/groovy/com/github/daggerok/eventsourcing/user/event/UserDeactivated.java +++ b/src/main/groovy/com/github/daggerok/eventsourcing/user/event/UserDeactivated.java @@ -1,15 +1,20 @@ package com.github.daggerok.eventsourcing.user.event; -import lombok.*; +import com.github.daggerok.eventsourcing.user.UserStatus; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.ToString; import java.time.ZonedDateTime; import java.util.UUID; @Getter @ToString -@AllArgsConstructor -@NoArgsConstructor(access = AccessLevel.PACKAGE) +@RequiredArgsConstructor +// @NoArgsConstructor(access = AccessLevel.PACKAGE) public class UserDeactivated implements DomainEvent { - UUID aggregateId; - ZonedDateTime at; + @NonNull UUID aggregateId; + final UserStatus state = UserStatus.SUSPENDED; + final ZonedDateTime at = ZonedDateTime.now(); } diff --git a/src/test/groovy/com/github/daggerok/eventsourcing/user/UserRepositoryTest.groovy b/src/test/groovy/com/github/daggerok/eventsourcing/user/UserRepositoryTest.groovy index 5cda1a5..bc3dfc7 100644 --- a/src/test/groovy/com/github/daggerok/eventsourcing/user/UserRepositoryTest.groovy +++ b/src/test/groovy/com/github/daggerok/eventsourcing/user/UserRepositoryTest.groovy @@ -8,7 +8,9 @@ class UserRepositoryTest extends Specification { def 'save user operation should flush user eventStream'() { given: - def user = new User(UUID.randomUUID()) + def user = new User() + and: + user.create() and: user.activate() when: @@ -21,7 +23,9 @@ class UserRepositoryTest extends Specification { given: def userId = UUID.randomUUID() and: - def user = new User(userId) + def user = new User() + and: + user.create(userId) and: user.activate() and: diff --git a/src/test/groovy/com/github/daggerok/eventsourcing/user/UserTest.groovy b/src/test/groovy/com/github/daggerok/eventsourcing/user/UserTest.groovy index 6aead8e..7da3da2 100644 --- a/src/test/groovy/com/github/daggerok/eventsourcing/user/UserTest.groovy +++ b/src/test/groovy/com/github/daggerok/eventsourcing/user/UserTest.groovy @@ -5,15 +5,19 @@ import spock.lang.Specification class UserTest extends Specification { def 'created user should have pending state'() { + given: + def user = new User() when: - def user = new User(UUID.randomUUID()) + user.create() then: user.state == UserStatus.PENDING } def 'active user should not be activated'() { given: - def user = new User(UUID.randomUUID()) + def user = new User() + and: + user.create() and: user.activate() when: @@ -24,7 +28,9 @@ class UserTest extends Specification { def 'created user can be activated'() { given: - def user = new User(UUID.randomUUID()) + def user = new User() + and: + user.create() when: user.activate() then: @@ -33,7 +39,9 @@ class UserTest extends Specification { def 'suspended user cannot be deactivated'() { given: - def user = new User(UUID.randomUUID()) + def user = new User() + and: + user.create() and: user.deactivate() when: @@ -44,7 +52,9 @@ class UserTest extends Specification { def 'active user can be deactivated'() { given: - def user = new User(UUID.randomUUID()) + def user = new User() + and: + user.create() and: user.activate() when: