Transaction aggregate implementation

This commit is contained in:
Michal Zeman
2019-09-15 11:03:02 +02:00
parent 1304089c66
commit e05f86aa6d
16 changed files with 393 additions and 62 deletions

View File

@@ -4,27 +4,8 @@ import com.mz.reactor.ddd.common.api.command.CommandHandler;
import com.mz.reactor.ddd.common.api.command.CommandResult;
import com.mz.reactor.ddd.reactorddd.account.domain.AccountAggregate;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiFunction;
public class AccountCommandHandler implements CommandHandler<AccountAggregate, AccountCommand> {
private final Map<Class, BiFunction> handlers = new HashMap<>();
public AccountCommandHandler() {
addHandler(CreateAccount.class, this::doCreateAccount);
addHandler(WithdrawMoney.class, this::doWithdrawMoney);
addHandler(DepositMoney.class, this::doDepositMoney);
}
private <K extends AccountCommand> void addHandler(
Class<K> kClass,
BiFunction<AccountAggregate, K, CommandResult> handler
) {
this.handlers.put(kClass, handler);
}
private CommandResult doWithdrawMoney(AccountAggregate aggregate, WithdrawMoney command) {
return null;
}
@@ -39,6 +20,14 @@ public class AccountCommandHandler implements CommandHandler<AccountAggregate, A
@Override
public CommandResult execute(AccountAggregate aggregate, AccountCommand command) {
return (CommandResult) handlers.get(command.getClass()).apply(aggregate, command);
if (command instanceof WithdrawMoney) {
return doWithdrawMoney(aggregate, (WithdrawMoney) command);
} else if (command instanceof CreateAccount) {
return doCreateAccount(aggregate, (CreateAccount) command);
} else if (command instanceof DepositMoney) {
return doDepositMoney(aggregate, (DepositMoney) command);
} else {
return CommandResult.badCommand();
}
}
}

View File

@@ -9,21 +9,6 @@ import java.util.function.BiFunction;
public class AccountEventApplier implements EventApplier<AccountAggregate, AccountEvent> {
private final Map<Class, BiFunction> appliers = new HashMap<>();
public AccountEventApplier() {
addApplier(AccountCreated.class, this::applyAccountCreated);
addApplier(MoneyWithdrawn.class, this::applyMoneyWithdrawn);
addApplier(MoneyDeposited.class, this::applyMoneyDeposited);
}
private <E extends AccountEvent> void addApplier(
Class<E> eClass,
BiFunction<AccountAggregate, E, AccountAggregate> applier
) {
appliers.put(eClass, applier);
}
private AccountAggregate applyAccountCreated(AccountAggregate aggregate, AccountCreated event) {
return aggregate;
}
@@ -38,6 +23,14 @@ public class AccountEventApplier implements EventApplier<AccountAggregate, Accou
@Override
public AccountAggregate apply(AccountAggregate aggregate, AccountEvent event) {
return (AccountAggregate) appliers.get(event.getClass()).apply(aggregate, event);
if (event instanceof AccountCreated) {
return applyAccountCreated(aggregate, (AccountCreated) event);
} else if (event instanceof MoneyWithdrawn) {
return applyMoneyWithdrawn(aggregate, (MoneyWithdrawn) event);
} else if (event instanceof MoneyDeposited) {
return applyMoneyDeposited(aggregate, (MoneyDeposited) event);
} else {
return aggregate;
}
}
}

View File

@@ -1,15 +1,20 @@
package com.mz.reactor.ddd.reactorddd.transaction.domain;
import com.mz.reactor.ddd.common.api.valueobject.Id;
import com.mz.reactor.ddd.reactorddd.transaction.domain.command.CreateTransaction;
import com.mz.reactor.ddd.reactorddd.transaction.domain.command.FinishTransaction;
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionCreated;
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionFinished;
import java.math.BigDecimal;
public class TransactionAggregate {
enum TransactionState {
Created,
Finished,
Failed
enum State {
INITIALIZED,
CREATED,
FINISHED,
FAILED
}
private Id aggregateId;
@@ -20,7 +25,60 @@ public class TransactionAggregate {
private BigDecimal amount;
private State state;
public TransactionAggregate(String aggregateId) {
this.aggregateId = new Id(aggregateId);
state = State.INITIALIZED;
}
public TransactionCreated validateCreateTransaction(CreateTransaction command) {
if (state == State.INITIALIZED) {
Id.validate(command.fromAccountId(), command.toAccountId());
if (command.amount().compareTo(BigDecimal.ZERO) <= 0) {
throw new RuntimeException(String.format("Amount can't be %s", command.amount()));
}
return TransactionCreated.builder()
.aggregateId(this.aggregateId.getValue())
.correlationId(command.correlationId())
.amount(command.amount())
.fromAccountId(command.fromAccountId())
.toAccountId(command.toAccountId())
.build();
} else {
throw new RuntimeException(String.format("Can't be applied command %s, aggregate is in state %s",
command, this.state));
}
}
public TransactionFinished validateFinishTransaction(FinishTransaction command) {
if (state == State.CREATED) {
return TransactionFinished.builder()
.aggregateId(aggregateId.getValue())
.correlationId(command.correlationId())
.fromAccountId(fromAccount.getValue())
.toAccountId(toAccount.getValue())
.build();
} else {
throw new RuntimeException(String.format("Transaction in the state: %s can't be finished!", state));
}
}
public TransactionAggregate applyTransactionCreated(TransactionCreated created) {
this.fromAccount = new Id(created.fromAccountId());
this.toAccount = new Id(created.toAccountId());
this.amount = created.amount();
this.state = State.CREATED;
return this;
}
public TransactionState getState() {
return TransactionState.builder()
.amount(amount)
.fromAccountId(fromAccount.getValue())
.toAccountId(toAccount.getValue())
.aggregateId(aggregateId.getValue())
.build();
}
}

View File

@@ -6,7 +6,6 @@ import java.math.BigDecimal;
@Value.Immutable
public interface CreateTransaction extends TransactionCommand {
String aggregateId();
String fromAccountId();

View File

@@ -0,0 +1,15 @@
package com.mz.reactor.ddd.reactorddd.transaction.domain.command;
import org.immutables.value.Value;
@Value.Immutable
public interface FinishTransaction extends TransactionCommand {
String fromAccountId();
String toAccountId();
static ImmutableFinishTransaction.Builder builder() {
return ImmutableFinishTransaction.builder();
}
}

View File

@@ -3,4 +3,5 @@ package com.mz.reactor.ddd.reactorddd.transaction.domain.command;
import com.mz.reactor.ddd.common.api.command.Command;
public interface TransactionCommand extends Command {
String aggregateId();
}

View File

@@ -2,33 +2,42 @@ package com.mz.reactor.ddd.reactorddd.transaction.domain.command;
import com.mz.reactor.ddd.common.api.command.CommandHandler;
import com.mz.reactor.ddd.common.api.command.CommandResult;
import com.mz.reactor.ddd.common.api.command.ImmutableCommandResult;
import com.mz.reactor.ddd.reactorddd.transaction.domain.TransactionAggregate;
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionFailed;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.List;
import java.util.Optional;
public class TransactionCommandHandler implements CommandHandler<TransactionAggregate, TransactionCommand> {
private final Map<Class, BiFunction> handlers = new HashMap<>();
public TransactionCommandHandler() {
addHandler(CreateTransaction.class, this::doCreateTransaction);
}
private <C extends TransactionCommand> void addHandler(
Class<C> kClass,
BiFunction<TransactionAggregate, C, CommandResult> handler
) {
handlers.put(kClass, handler);
}
private CommandResult doCreateTransaction(TransactionAggregate aggregate, CreateTransaction command) {
return (CommandResult) handlers.get(command).apply(aggregate, command);
try {
return Optional.ofNullable(aggregate.validateCreateTransaction(command))
.map(e -> CommandResult.builder()
.events(List.of(e))
.statusCode(CommandResult.StatusCode.OK)
.build())
.orElseGet(() -> (ImmutableCommandResult) CommandResult.notModified());
} catch (RuntimeException e) {
return CommandResult.builder()
.events(List.of(TransactionFailed.from(command)))
.statusCode(CommandResult.StatusCode.FAILED)
.error(e)
.build();
}
}
@Override
public CommandResult execute(TransactionAggregate aggregate, TransactionCommand command) {
return null;
if (command instanceof CreateTransaction) {
return doCreateTransaction(aggregate, (CreateTransaction) command);
} else {
return CommandResult.badCommand();
}
}
}

View File

@@ -6,7 +6,7 @@ import java.math.BigDecimal;
@Value.Immutable
public interface TransactionCreated extends TransactionEvent {
String fromAccountId();
String toAccountId();

View File

@@ -0,0 +1,21 @@
package com.mz.reactor.ddd.reactorddd.transaction.domain.event;
import com.mz.reactor.ddd.common.api.event.EventApplier;
import com.mz.reactor.ddd.reactorddd.transaction.domain.TransactionAggregate;
public class TransactionEventApplier implements EventApplier<TransactionAggregate, TransactionEvent> {
@Override
public TransactionAggregate apply(TransactionAggregate aggregate, TransactionEvent event) {
if (event instanceof TransactionCreated) {
return applyTransactionCreated(aggregate, (TransactionCreated) event);
} else {
return aggregate;
}
}
private TransactionAggregate applyTransactionCreated(TransactionAggregate aggregate, TransactionCreated event) {
return aggregate.applyTransactionCreated(event);
}
}

View File

@@ -1,5 +1,6 @@
package com.mz.reactor.ddd.reactorddd.transaction.domain.event;
import com.mz.reactor.ddd.reactorddd.transaction.domain.command.CreateTransaction;
import org.immutables.value.Value;
import java.math.BigDecimal;
@@ -17,4 +18,13 @@ public interface TransactionFailed extends TransactionEvent {
return ImmutableTransactionFailed.builder();
}
static TransactionFailed from(CreateTransaction command) {
return TransactionFailed.builder()
.toAccountId(command.toAccountId())
.fromAccountId(command.fromAccountId())
.aggregateId(command.aggregateId())
.correlationId(command.correlationId())
.amount(command.amount())
.build();
}
}

View File

@@ -5,6 +5,10 @@ import org.immutables.value.Value;
@Value.Immutable
public interface TransactionFinished extends TransactionEvent {
String fromAccountId();
String toAccountId();
static ImmutableTransactionFinished.Builder builder() {
return ImmutableTransactionFinished.builder();
}

View File

@@ -0,0 +1,125 @@
package com.mz.reactor.ddd.reactorddd.transaction.domain;
import com.mz.reactor.ddd.reactorddd.transaction.domain.command.CreateTransaction;
import com.mz.reactor.ddd.reactorddd.transaction.domain.command.FinishTransaction;
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionCreated;
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionFinished;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
class TransactionAggregateTest {
@Test
void validateCreateTransaction_TransactionCreatedTest() {
var aggregateId = UUID.randomUUID().toString();
var correlationId = UUID.randomUUID().toString();
var toAccountId = UUID.randomUUID().toString();
var fromAccountId = UUID.randomUUID().toString();
var command = CreateTransaction.builder()
.aggregateId(aggregateId)
.amount(BigDecimal.ONE)
.correlationId(correlationId)
.fromAccountId(fromAccountId)
.toAccountId(toAccountId)
.build();
var aggregate = new TransactionAggregate(aggregateId);
var result = aggregate.validateCreateTransaction(command);
assertTrue(result instanceof TransactionCreated);
assertEquals(result.correlationId().get(), correlationId);
assertEquals(result.aggregateId(), aggregateId);
assertEquals(result.toAccountId(), toAccountId);
assertEquals(result.fromAccountId(), fromAccountId);
}
@Test
void validateCreateTransaction_TransactionFailed_amount() {
var aggregateId = UUID.randomUUID().toString();
var correlationId = UUID.randomUUID().toString();
var toAccountId = UUID.randomUUID().toString();
var fromAccountId = UUID.randomUUID().toString();
var command = CreateTransaction.builder()
.aggregateId(aggregateId)
.amount(BigDecimal.ZERO)
.correlationId(correlationId)
.fromAccountId(fromAccountId)
.toAccountId(toAccountId)
.build();
var aggregate = new TransactionAggregate(aggregateId);
assertThrows(RuntimeException.class, () -> aggregate.validateCreateTransaction(command));
}
@Test
void applyTransactionCreated() {
var aggregateId = UUID.randomUUID().toString();
var correlationId = UUID.randomUUID().toString();
var toAccountId = UUID.randomUUID().toString();
var fromAccountId = UUID.randomUUID().toString();
var event = TransactionCreated.builder()
.aggregateId(aggregateId)
.correlationId(correlationId)
.amount(BigDecimal.TEN)
.fromAccountId(fromAccountId)
.toAccountId(toAccountId)
.build();
var aggregate = new TransactionAggregate(aggregateId);
var state = aggregate.applyTransactionCreated(event).getState();
assertEquals(state.aggregateId(), aggregateId);
assertEquals(state.toAccountId(), toAccountId);
assertEquals(state.fromAccountId(), fromAccountId);
assertEquals(state.amount(), BigDecimal.TEN);
}
@Test
void validateFinishTransaction_OK() {
var aggregateId = UUID.randomUUID().toString();
var correlationId = UUID.randomUUID().toString();
var toAccountId = UUID.randomUUID().toString();
var fromAccountId = UUID.randomUUID().toString();
var event = TransactionCreated.builder()
.aggregateId(aggregateId)
.correlationId(correlationId)
.amount(BigDecimal.TEN)
.fromAccountId(fromAccountId)
.toAccountId(toAccountId)
.build();
var aggregate = new TransactionAggregate(aggregateId);
aggregate.applyTransactionCreated(event);
var result = aggregate.validateFinishTransaction(FinishTransaction.builder()
.toAccountId(toAccountId)
.fromAccountId(fromAccountId)
.correlationId(correlationId)
.aggregateId(aggregateId)
.build());
assertTrue(result instanceof TransactionFinished);
assertEquals(result.correlationId().get(), correlationId);
assertEquals(result.aggregateId(), aggregateId);
}
@Test
void validateFinishTransaction_FAILED() {
var aggregateId = UUID.randomUUID().toString();
var correlationId = UUID.randomUUID().toString();
var toAccountId = UUID.randomUUID().toString();
var fromAccountId = UUID.randomUUID().toString();
var aggregate = new TransactionAggregate(aggregateId);
assertThrows(RuntimeException.class, () -> aggregate.validateFinishTransaction(FinishTransaction.builder()
.toAccountId(toAccountId)
.fromAccountId(fromAccountId)
.correlationId(correlationId)
.aggregateId(aggregateId)
.build()));
}
}

View File

@@ -0,0 +1,57 @@
package com.mz.reactor.ddd.reactorddd.transaction.domain.command;
import com.mz.reactor.ddd.common.api.command.CommandResult;
import com.mz.reactor.ddd.reactorddd.transaction.domain.TransactionAggregate;
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionCreated;
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionFailed;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
class TransactionCommandHandlerTest {
TransactionCommandHandler subject = new TransactionCommandHandler();
@Test
void execute_CreateTransaction_OK() {
var aggregateId = UUID.randomUUID().toString();
var correlationId = UUID.randomUUID().toString();
var toAccountId = UUID.randomUUID().toString();
var fromAccountId = UUID.randomUUID().toString();
var command = CreateTransaction.builder()
.aggregateId(aggregateId)
.amount(BigDecimal.TEN)
.correlationId(correlationId)
.fromAccountId(fromAccountId)
.toAccountId(toAccountId)
.build();
var aggregate = new TransactionAggregate(aggregateId);
var result = subject.execute(aggregate, command);
assertEquals(result.statusCode(), CommandResult.StatusCode.OK);
assertTrue(result.events().stream().allMatch(e -> e instanceof TransactionCreated));
}
@Test
void execute_CreateTransaction_FAILED() {
var aggregateId = UUID.randomUUID().toString();
var correlationId = UUID.randomUUID().toString();
var toAccountId = UUID.randomUUID().toString();
var fromAccountId = UUID.randomUUID().toString();
var command = CreateTransaction.builder()
.aggregateId(aggregateId)
.amount(BigDecimal.ZERO)
.correlationId(correlationId)
.fromAccountId(fromAccountId)
.toAccountId(toAccountId)
.build();
var aggregate = new TransactionAggregate(aggregateId);
var result = subject.execute(aggregate, command);
assertEquals(result.statusCode(), CommandResult.StatusCode.FAILED);
assertTrue(result.events().stream().allMatch(e -> e instanceof TransactionFailed));
}
}

View File

@@ -36,11 +36,15 @@ allprojects {
dependencyManagement {
imports { mavenBom("org.springframework.boot:spring-boot-dependencies:${springBootVersion}") }
}
test {
useJUnitPlatform{
includeEngines 'junit-jupiter'
}
}
}
subprojects {
// apply plugin: 'java'
// apply plugin: 'java-library'
group = 'com.mz.reactor.ddd'
version = '0.0.1-SNAPSHOT'
@@ -48,6 +52,8 @@ subprojects {
dependencies {
annotationProcessor 'org.immutables:value:2.7.5'
testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.5.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.5.2'
}
}
@@ -89,5 +95,6 @@ project(':bank-account:transaction-domain') {
project(':shared-dependencies') {
dependencies {
api 'org.immutables:value:2.7.5'
api group: 'org.apache.commons', name: 'commons-lang3', version: '3.9'
}
}

View File

@@ -1,14 +1,48 @@
package com.mz.reactor.ddd.common.api.command;
import com.mz.reactor.ddd.common.api.event.DomainEvent;
import org.immutables.value.Value;
import java.util.List;
import java.util.Optional;
@Value.Immutable
public interface CommandResult {
enum StatusCode {
OK,
BAD_COMMAND,
FAILED,
NOT_MODIFIED;
}
StatusCode statusCode();
List<DomainEvent> events();
Optional<RuntimeException> error();
static ImmutableCommandResult.Builder builder() {
return ImmutableCommandResult.builder();
}
static CommandResult fromError(RuntimeException error, DomainEvent event) {
return builder()
.statusCode(StatusCode.BAD_COMMAND)
.events(List.of(event))
.error(error)
.build();
}
static CommandResult badCommand() {
return builder()
.statusCode(StatusCode.BAD_COMMAND)
.build();
}
static CommandResult notModified() {
return builder()
.statusCode(StatusCode.NOT_MODIFIED)
.build();
}
}

View File

@@ -1,18 +1,27 @@
package com.mz.reactor.ddd.common.api.valueobject;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream;
public class StringValue {
private final String value;
public StringValue(String value) {
this.value = Optional.ofNullable(value)
.filter(v -> !v.isBlank())
.orElseThrow(() -> new RuntimeException("Value can't be null or empty"));
this.value = validateValue.apply(value);
}
public String getValue() {
return value;
}
public static Function<String, String> validateValue = value ->
Optional.ofNullable(value)
.filter(v -> !v.isBlank())
.orElseThrow(() -> new RuntimeException("Value can't be null or empty"));
public static void validate(String... values) {
Stream.of(values)
.map(validateValue);
}
}