transfer money operation

This commit is contained in:
Michal Zeman
2019-12-14 11:51:46 +01:00
parent 65381f7f50
commit edf063d0c4
72 changed files with 1421 additions and 276 deletions

View File

@@ -0,0 +1 @@
description('account-adapter')

View File

@@ -1,5 +1,7 @@
package com.mz.reactor.ddd.reactorddd.account.api;
import com.mz.reactor.ddd.common.api.event.DomainEvent;
import com.mz.reactor.ddd.reactorddd.account.domain.command.AccountCommand;
import com.mz.reactor.ddd.reactorddd.account.domain.command.CreateAccount;
import com.mz.reactor.ddd.reactorddd.account.domain.command.DepositMoney;
import com.mz.reactor.ddd.reactorddd.account.domain.command.WithdrawMoney;
@@ -16,4 +18,6 @@ public interface AccountApplicationService {
Mono<MoneyWithdrawn> execute(WithdrawMoney cmd);
<R extends DomainEvent> Mono<R> execute(AccountCommand cmd, Class<R> eventType);
}

View File

@@ -2,6 +2,7 @@ package com.mz.reactor.ddd.reactorddd.account.api;
import com.mz.reactor.ddd.common.components.http.HttpHandler;
import com.mz.reactor.ddd.reactorddd.account.api.model.*;
import com.mz.reactor.ddd.reactorddd.account.domain.AccountState;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.RouterFunction;
@@ -17,8 +18,22 @@ public class AccountHandler implements HttpHandler {
private final AccountApplicationService service;
public AccountHandler(AccountApplicationService service) {
private final AccountQuery accountQuery;
public AccountHandler(AccountApplicationService service, AccountQuery accountQuery) {
this.service = service;
this.accountQuery = accountQuery;
}
public Mono<ServerResponse> getById(ServerRequest request) {
return accountQuery.findById(request.pathVariable("id"))
.flatMap(this::mapToResponse);
}
public Mono<ServerResponse> getAll(ServerRequest request) {
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(accountQuery.getAll(), AccountState.class);
}
public Mono<ServerResponse> createAccount(ServerRequest request) {
@@ -51,8 +66,10 @@ public class AccountHandler implements HttpHandler {
public RouterFunction<ServerResponse> route() {
var route = RouterFunctions
.route(POST("").and(accept(MediaType.APPLICATION_JSON_UTF8)), this::createAccount)
.andRoute(GET("/").and(accept(MediaType.APPLICATION_JSON_UTF8)), this::getAll)
.andRoute(GET("/{id}").and(accept(MediaType.APPLICATION_JSON_UTF8)), this::getById)
.andRoute(PUT("/moneys/withdraw").and(accept(MediaType.APPLICATION_JSON_UTF8)), this::withdrawMoney)
.andRoute(PUT("/moneys/deposit").and(accept(MediaType.APPLICATION_JSON_UTF8)), this::withdrawMoney);
.andRoute(PUT("/moneys/deposit").and(accept(MediaType.APPLICATION_JSON_UTF8)), this::depositMoney);
return RouterFunctions.route()
.nest(path("/accounts"), () -> route)

View File

@@ -1,4 +1,11 @@
package com.mz.reactor.ddd.reactorddd.account.api;
import com.mz.reactor.ddd.reactorddd.account.domain.AccountState;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface AccountQuery {
Mono<AccountState> findById(String id);
Flux<AccountState> getAll();
}

View File

@@ -0,0 +1,33 @@
package com.mz.reactor.ddd.reactorddd.account.domain;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.mz.reactor.ddd.common.api.view.DomainView;
import org.immutables.value.Value;
import java.math.BigDecimal;
import java.util.List;
@Value.Immutable
@JsonSerialize(as = ImmutableAccountState.class)
@JsonDeserialize(as = ImmutableAccountState.class)
@JsonInclude(JsonInclude.Include.NON_NULL)
public interface AccountState extends DomainView {
default String id() {
return aggregateId();
}
String aggregateId();
BigDecimal amount();
@JsonIgnore
List<String> openedTransactions();
static ImmutableAccountState.Builder builder() {
return ImmutableAccountState.builder();
}
}

View File

@@ -5,12 +5,18 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.immutables.value.Value;
import java.math.BigDecimal;
import java.util.UUID;
@Value.Immutable
@JsonSerialize(as = ImmutableCreateAccount.class)
@JsonDeserialize(as = ImmutableCreateAccount.class)
public interface CreateAccount extends AccountCommand {
@Value.Default
default String aggregateId() {
return UUID.randomUUID().toString();
}
BigDecimal balance();
static ImmutableCreateAccount.Builder builder() {

View File

@@ -0,0 +1,16 @@
package com.mz.reactor.ddd.reactorddd.account.domain.command;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.immutables.value.Value;
@Value.Immutable
@JsonSerialize(as = ImmutableDepositTransferMoney.class)
@JsonDeserialize(as = ImmutableDepositTransferMoney.class)
public interface DepositTransferMoney extends TransferMoneyCommand {
static ImmutableDepositTransferMoney.Builder builder() {
return ImmutableDepositTransferMoney.builder();
}
}

View File

@@ -0,0 +1,15 @@
package com.mz.reactor.ddd.reactorddd.account.domain.command;
import java.math.BigDecimal;
public interface TransferMoneyCommand extends AccountCommand {
String transactionId();
String fromAccount();
String toAccount();
BigDecimal amount();
}

View File

@@ -0,0 +1,18 @@
package com.mz.reactor.ddd.reactorddd.account.domain.command;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.immutables.value.Value;
import java.math.BigDecimal;
@Value.Immutable
@JsonSerialize(as = ImmutableWithdrawTransferMoney.class)
@JsonDeserialize(as = ImmutableWithdrawTransferMoney.class)
public interface WithdrawTransferMoney extends TransferMoneyCommand {
static ImmutableWithdrawTransferMoney.Builder builder() {
return ImmutableWithdrawTransferMoney.builder();
}
}

View File

@@ -0,0 +1,15 @@
package com.mz.reactor.ddd.reactorddd.account.domain.event;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.immutables.value.Value;
@Value.Immutable
@JsonSerialize(as = ImmutableDepositTransferMoneyFailed.class)
@JsonDeserialize(as = ImmutableDepositTransferMoneyFailed.class)
public interface DepositTransferMoneyFailed extends TransferMoneyFailed {
static ImmutableDepositTransferMoneyFailed.Builder builder() {
return ImmutableDepositTransferMoneyFailed.builder();
}
}

View File

@@ -0,0 +1,18 @@
package com.mz.reactor.ddd.reactorddd.account.domain.event;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.immutables.value.Value;
import java.math.BigDecimal;
@Value.Immutable
@JsonSerialize(as = ImmutableTransferMoneyDeposited.class)
@JsonDeserialize(as = ImmutableTransferMoneyDeposited.class)
public interface TransferMoneyDeposited extends TransferMoneyEvent {
static ImmutableTransferMoneyDeposited.Builder builder() {
return ImmutableTransferMoneyDeposited.builder();
}
}

View File

@@ -0,0 +1,15 @@
package com.mz.reactor.ddd.reactorddd.account.domain.event;
import java.math.BigDecimal;
public interface TransferMoneyEvent extends AccountEvent {
String transactionId();
String fromAccount();
String toAccount();
BigDecimal amount();
}

View File

@@ -0,0 +1,14 @@
package com.mz.reactor.ddd.reactorddd.account.domain.event;
import java.math.BigDecimal;
public interface TransferMoneyFailed extends AccountEvent {
String transactionId();
String fromAccount();
String toAccount();
BigDecimal amount();
}

View File

@@ -0,0 +1,30 @@
package com.mz.reactor.ddd.reactorddd.account.domain.event;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.mz.reactor.ddd.reactorddd.account.domain.command.WithdrawTransferMoney;
import org.immutables.value.Value;
import java.math.BigDecimal;
@Value.Immutable
@JsonSerialize(as = ImmutableTransferMoneyWithdrawn.class)
@JsonDeserialize(as = ImmutableTransferMoneyWithdrawn.class)
public interface TransferMoneyWithdrawn extends TransferMoneyEvent {
static ImmutableTransferMoneyWithdrawn.Builder builder() {
return ImmutableTransferMoneyWithdrawn.builder();
}
static TransferMoneyWithdrawn from(WithdrawTransferMoney cmd) {
return TransferMoneyWithdrawn.builder()
.transactionId(cmd.transactionId())
.aggregateId(cmd.aggregateId())
.amount(cmd.amount())
.correlationId(cmd.correlationId())
.fromAccount(cmd.fromAccount())
.toAccount(cmd.toAccount())
.build();
}
}

View File

@@ -0,0 +1,14 @@
package com.mz.reactor.ddd.reactorddd.account.domain.event;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.immutables.value.Value;
@Value.Immutable
@JsonSerialize(as = ImmutableWithdrawTransferMoneyFailed.class)
@JsonDeserialize(as = ImmutableWithdrawTransferMoneyFailed.class)
public interface WithdrawTransferMoneyFailed extends TransferMoneyFailed {
static ImmutableWithdrawTransferMoneyFailed.Builder builder() {
return ImmutableWithdrawTransferMoneyFailed.builder();
}
}

View File

@@ -2,12 +2,8 @@ package com.mz.reactor.ddd.reactorddd.account.domain;
import com.mz.reactor.ddd.common.api.valueobject.Id;
import com.mz.reactor.ddd.common.api.valueobject.Money;
import com.mz.reactor.ddd.reactorddd.account.domain.command.CreateAccount;
import com.mz.reactor.ddd.reactorddd.account.domain.command.DepositMoney;
import com.mz.reactor.ddd.reactorddd.account.domain.command.WithdrawMoney;
import com.mz.reactor.ddd.reactorddd.account.domain.event.AccountCreated;
import com.mz.reactor.ddd.reactorddd.account.domain.event.MoneyDeposited;
import com.mz.reactor.ddd.reactorddd.account.domain.event.MoneyWithdrawn;
import com.mz.reactor.ddd.reactorddd.account.domain.command.*;
import com.mz.reactor.ddd.reactorddd.account.domain.event.*;
import java.math.BigDecimal;
import java.util.ArrayList;
@@ -50,6 +46,27 @@ public class AccountAggregate {
.apply(command.balance());
}
public TransferMoneyWithdrawn validateWithdrawTransferMoney(WithdrawTransferMoney command) {
return Money.validateValue
.andThen(v -> Money.validateWithdraw.apply(this.amount.getAmount(), command.amount()))
.andThen(v -> TransferMoneyWithdrawn.from(command))
.apply(command.amount());
}
public TransferMoneyDeposited validateDepositTransferMoney(DepositTransferMoney depositMoney) {
return Money.validateDepositMoney
.andThen(v ->
TransferMoneyDeposited.builder()
.correlationId(depositMoney.correlationId())
.aggregateId(aggregateId.getValue())
.amount(v)
.transactionId(depositMoney.transactionId())
.fromAccount(depositMoney.fromAccount())
.toAccount(depositMoney.toAccount())
.build())
.apply(depositMoney.amount());
}
public AccountAggregate applyMoneyDeposited(MoneyDeposited moneyDeposited) {
this.amount = this.amount.depositMoney(moneyDeposited.amount());
return this;
@@ -65,6 +82,16 @@ public class AccountAggregate {
return this;
}
public AccountAggregate applyTransferMoneyWithdrawn(TransferMoneyWithdrawn event) {
this.amount = this.amount.withdrawMoney(event.amount());
return this;
}
public AccountAggregate applyTransferMoneyDeposited(TransferMoneyDeposited event) {
this.amount = this.amount.depositMoney(event.amount());
return this;
}
public AccountState getState() {
return AccountState.builder()
.aggregateId(this.aggregateId.getValue())

View File

@@ -1,25 +1,34 @@
package com.mz.reactor.ddd.reactorddd.account.domain;
import com.mz.reactor.ddd.common.api.command.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.account.domain.command.AccountCommand;
import com.mz.reactor.ddd.reactorddd.account.domain.command.CreateAccount;
import com.mz.reactor.ddd.reactorddd.account.domain.command.DepositMoney;
import com.mz.reactor.ddd.reactorddd.account.domain.command.WithdrawMoney;
import com.mz.reactor.ddd.reactorddd.account.domain.event.CreateAccountFailed;
import com.mz.reactor.ddd.reactorddd.account.domain.event.DepositMoneyFailed;
import com.mz.reactor.ddd.reactorddd.account.domain.event.WithdrawMoneyFailed;
import com.mz.reactor.ddd.reactorddd.account.domain.command.*;
import com.mz.reactor.ddd.reactorddd.account.domain.event.*;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Function;
public class AccountCommandHandler implements CommandHandler<AccountAggregate, AccountCommand> {
private final Function<AccountCommand, ImmutableCommandResult> commandNotModified = cmd ->
(ImmutableCommandResult) CommandResult.notModified(cmd);
@Override
public CommandResult execute(AccountAggregate aggregate, AccountCommand 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 if (command instanceof WithdrawTransferMoney) {
return doWithdrawTransferMoney(aggregate, (WithdrawTransferMoney) command);
} else if (command instanceof DepositTransferMoney) {
return doDepositTransferMoney(aggregate, (DepositTransferMoney) command);
} else {
return CommandResult.badCommand(command);
}
}
private CommandResult doWithdrawMoney(AccountAggregate aggregate, WithdrawMoney command) {
try {
@@ -28,22 +37,15 @@ public class AccountCommandHandler implements CommandHandler<AccountAggregate, A
.map(e -> CommandResult.builder()
.commandId(command.commandId())
.statusCode(CommandResult.StatusCode.OK)
.addEvents(e)
.events(List.of(e))
.build())
.orElseGet(() -> commandNotModified.apply(command));
.orElseGet(() -> (ImmutableCommandResult) CommandResult.notModified(command));
} catch (RuntimeException e) {
return CommandResult.builder()
.commandId(Optional.ofNullable(command)
.map(Command::commandId)
.orElseGet(() -> UUID.randomUUID().toString()))
.addEvents(WithdrawMoneyFailed.builder()
.aggregateId(command.aggregateId())
.amount(command.amount())
.correlationId(command.correlationId())
.build())
.statusCode(CommandResult.StatusCode.FAILED)
.error(e)
.build();
return CommandResult.fromError(e, List.of(WithdrawMoneyFailed.builder()
.aggregateId(command.aggregateId())
.amount(command.amount())
.correlationId(command.correlationId())
.build()), command);
}
}
@@ -54,22 +56,19 @@ public class AccountCommandHandler implements CommandHandler<AccountAggregate, A
.map(e -> CommandResult.builder()
.commandId(command.commandId())
.statusCode(CommandResult.StatusCode.OK)
.addEvents(e)
.events(List.of(e))
.build())
.orElseGet(() -> commandNotModified.apply(command));
.orElseGet(() -> (ImmutableCommandResult) CommandResult.notModified(command));
} catch (RuntimeException e) {
return CommandResult.builder()
.commandId(Optional.ofNullable(command)
.map(Command::commandId)
.orElseGet(() -> UUID.randomUUID().toString()))
.addEvents(CreateAccountFailed.builder()
return CommandResult.fromError(
e,
List.of(CreateAccountFailed.builder()
.aggregateId(command.aggregateId())
.balance(command.balance())
.correlationId(command.correlationId())
.build())
.statusCode(CommandResult.StatusCode.FAILED)
.error(e)
.build();
.build()),
command
);
}
}
@@ -80,35 +79,71 @@ public class AccountCommandHandler implements CommandHandler<AccountAggregate, A
.map(e -> CommandResult.builder()
.commandId(command.commandId())
.statusCode(CommandResult.StatusCode.OK)
.addEvents(e)
.events(List.of(e))
.build())
.orElseGet(() -> commandNotModified.apply(command));
.orElseGet(() -> (ImmutableCommandResult) CommandResult.notModified(command));
} catch (RuntimeException e) {
return CommandResult.builder()
.commandId(Optional.ofNullable(command)
.map(Command::commandId)
.orElseGet(() -> UUID.randomUUID().toString()))
.addEvents(DepositMoneyFailed.builder()
return CommandResult.fromError(
e,
List.of(DepositMoneyFailed.builder()
.aggregateId(command.aggregateId())
.amount(command.amount())
.correlationId(command.correlationId())
.build())
.statusCode(CommandResult.StatusCode.FAILED)
.error(e)
.build();
.build()),
command
);
}
}
@Override
public CommandResult execute(AccountAggregate aggregate, AccountCommand 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(command);
private CommandResult doWithdrawTransferMoney(AccountAggregate aggregate, WithdrawTransferMoney command) {
try {
return Optional.of(command)
.map(aggregate::validateWithdrawTransferMoney)
.map(e -> CommandResult.builder()
.commandId(command.commandId())
.statusCode(CommandResult.StatusCode.OK)
.events(List.of(e))
.build())
.orElseGet(() -> (ImmutableCommandResult) CommandResult.notModified(command));
} catch (RuntimeException e) {
return CommandResult.fromError(
e,
List.of(WithdrawTransferMoneyFailed.builder()
.transactionId(command.transactionId())
.aggregateId(command.aggregateId())
.amount(command.amount())
.correlationId(command.correlationId())
.fromAccount(command.fromAccount())
.toAccount(command.toAccount())
.build()),
command
);
}
}
private CommandResult doDepositTransferMoney(AccountAggregate aggregate, DepositTransferMoney command) {
try {
return Optional.of(command)
.map(aggregate::validateDepositTransferMoney)
.map(e -> CommandResult.builder()
.commandId(command.commandId())
.statusCode(CommandResult.StatusCode.OK)
.events(List.of(e))
.build())
.orElseGet(() -> (ImmutableCommandResult) CommandResult.notModified(command));
} catch (RuntimeException e) {
return CommandResult.fromError(
e,
List.of(DepositTransferMoneyFailed.builder()
.transactionId(command.transactionId())
.aggregateId(command.aggregateId())
.amount(command.amount())
.correlationId(command.correlationId())
.fromAccount(command.fromAccount())
.toAccount(command.toAccount())
.build()),
command
);
}
}
}

View File

@@ -1,13 +1,31 @@
package com.mz.reactor.ddd.reactorddd.account.domain;
import com.mz.reactor.ddd.common.api.event.DomainEvent;
import com.mz.reactor.ddd.common.api.event.EventApplier;
import com.mz.reactor.ddd.reactorddd.account.domain.AccountAggregate;
import com.mz.reactor.ddd.reactorddd.account.domain.event.AccountCreated;
import com.mz.reactor.ddd.reactorddd.account.domain.event.AccountEvent;
import com.mz.reactor.ddd.reactorddd.account.domain.event.MoneyDeposited;
import com.mz.reactor.ddd.reactorddd.account.domain.event.MoneyWithdrawn;
import com.mz.reactor.ddd.reactorddd.account.domain.event.*;
public class AccountEventApplier implements EventApplier<AccountAggregate, AccountEvent> {
public class AccountEventApplier implements EventApplier<AccountAggregate> {
@Override
public <E extends DomainEvent> AccountAggregate apply(AccountAggregate aggregate, E 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 if (event instanceof TransferMoneyWithdrawn) {
return applyTransferMoneyWithdrawn(aggregate, (TransferMoneyWithdrawn) event);
} else if (event instanceof TransferMoneyDeposited) {
return applyTransferMoneyDeposited(aggregate, (TransferMoneyDeposited) event);
} else {
return aggregate;
}
}
private AccountAggregate applyTransferMoneyDeposited(AccountAggregate aggregate, TransferMoneyDeposited event) {
return aggregate.applyTransferMoneyDeposited(event);
}
private AccountAggregate applyAccountCreated(AccountAggregate aggregate, AccountCreated event) {
return aggregate.applyAccountCreated(event);
@@ -21,16 +39,7 @@ public class AccountEventApplier implements EventApplier<AccountAggregate, Accou
return aggregate.applyMoneyDeposited(event);
}
@Override
public AccountAggregate apply(AccountAggregate aggregate, AccountEvent 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;
}
private AccountAggregate applyTransferMoneyWithdrawn(AccountAggregate aggregate, TransferMoneyWithdrawn event) {
return aggregate.applyTransferMoneyWithdrawn(event);
}
}

View File

@@ -1,19 +0,0 @@
package com.mz.reactor.ddd.reactorddd.account.domain;
import org.immutables.value.Value;
import java.math.BigDecimal;
import java.util.List;
@Value.Immutable
public interface AccountState {
String aggregateId();
BigDecimal amount();
List<String> openedTransactions();
static ImmutableAccountState.Builder builder() {
return ImmutableAccountState.builder();
}
}

View File

@@ -2,6 +2,7 @@ package com.mz.reactor.ddd.reactorddd.account.domain;
import com.mz.reactor.ddd.reactorddd.account.domain.command.CreateAccount;
import com.mz.reactor.ddd.reactorddd.account.domain.command.DepositMoney;
import com.mz.reactor.ddd.reactorddd.account.domain.command.WithdrawTransferMoney;
import com.mz.reactor.ddd.reactorddd.account.domain.command.WithdrawMoney;
import com.mz.reactor.ddd.reactorddd.account.domain.event.AccountCreated;
import com.mz.reactor.ddd.reactorddd.account.domain.event.MoneyDeposited;
@@ -151,6 +152,40 @@ class AccountAggregateTest {
.build();
//then
Assertions.assertThrows(RuntimeException.class, () -> aggregate.validateWithdrawMoney(withdrawMoney));
Assertions.assertThrows(RuntimeException.class, () -> aggregate.validateWithdrawMoney(withdrawMoney));
}
@Test
void applyMoneyTransferred() {
//given
var toAccountId = UUID.randomUUID().toString();
var aggregateId = UUID.randomUUID().toString();
var correlationId = UUID.randomUUID().toString();
var transactionId = UUID.randomUUID().toString();
var aggregate = new AccountAggregate(aggregateId);
var accountCreated = AccountCreated.builder()
.aggregateId(aggregateId)
.correlationId(correlationId)
.balance(BigDecimal.TEN)
.build();
aggregate.applyAccountCreated(accountCreated).getState();
var command = WithdrawTransferMoney.builder()
.transactionId(transactionId)
.aggregateId(aggregateId)
.fromAccount(aggregateId)
.toAccount(toAccountId)
.amount(BigDecimal.TEN)
.build();
Assertions.assertEquals(aggregate.getState().amount(), BigDecimal.TEN);
//when
var event = aggregate.validateWithdrawTransferMoney(command);
var state = aggregate.applyTransferMoneyWithdrawn(event).getState();
//then
Assertions.assertEquals(state.aggregateId(), aggregateId);
Assertions.assertEquals(state.amount(), BigDecimal.ZERO);
}
}

View File

@@ -1,46 +1,108 @@
package com.mz.reactor.ddd.reactorddd.account.impl;
import com.mz.reactor.ddd.common.api.event.DomainEvent;
import com.mz.reactor.ddd.common.components.bus.ApplicationMessageBus;
import com.mz.reactor.ddd.reactorddd.account.api.AccountApplicationService;
import com.mz.reactor.ddd.reactorddd.account.domain.AccountAggregate;
import com.mz.reactor.ddd.reactorddd.account.domain.AccountState;
import com.mz.reactor.ddd.reactorddd.account.domain.command.AccountCommand;
import com.mz.reactor.ddd.reactorddd.account.domain.command.CreateAccount;
import com.mz.reactor.ddd.reactorddd.account.domain.command.DepositMoney;
import com.mz.reactor.ddd.reactorddd.account.domain.command.WithdrawMoney;
import com.mz.reactor.ddd.reactorddd.account.domain.event.AccountCreated;
import com.mz.reactor.ddd.reactorddd.account.domain.event.AccountEvent;
import com.mz.reactor.ddd.reactorddd.account.domain.event.MoneyDeposited;
import com.mz.reactor.ddd.reactorddd.account.domain.event.MoneyWithdrawn;
import com.mz.reactor.ddd.reactorddd.account.domain.command.*;
import com.mz.reactor.ddd.reactorddd.account.domain.event.*;
import com.mz.reactor.ddd.reactorddd.persistance.aggregate.AggregateFacade;
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionCreated;
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionFailed;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import java.util.Optional;
@Service
public class AccountApplicationServiceImpl implements AccountApplicationService {
private final AggregateFacade<AccountAggregate, AccountCommand, AccountEvent, AccountState> aggregateFacade;
private static final Log log = LogFactory.getLog(AccountApplicationServiceImpl.class);
private final AggregateFacade<AccountAggregate, AccountCommand, AccountState> aggregateFacade;
public AccountApplicationServiceImpl(
ApplicationMessageBus bus,
@Qualifier("accountAggregateFacade") AggregateFacade<AccountAggregate, AccountCommand, AccountEvent, AccountState> aggregateFacade
@Qualifier("accountAggregateFacade") AggregateFacade<AccountAggregate, AccountCommand, AccountState> aggregateFacade
) {
this.aggregateFacade = aggregateFacade;
handleTransactionCreated(bus);
handleTransferMoneyWithdrawn(bus);
}
public <R extends DomainEvent> Mono<R> execute(AccountCommand cmd, Class<R> eventType) {
return this.aggregateFacade.executeReturnEvent(cmd, cmd.aggregateId(), eventType)
.cast(eventType);
}
@Override
public Mono<AccountCreated> execute(CreateAccount cmd) {
return this.aggregateFacade.executeReturnEvent(cmd, cmd.aggregateId()).cast(AccountCreated.class);
return this.aggregateFacade.executeReturnEvent(cmd, cmd.aggregateId(), AccountCreated.class)
.cast(AccountCreated.class);
}
@Override
public Mono<MoneyDeposited> execute(DepositMoney cmd) {
return this.aggregateFacade.executeReturnEvent(cmd, cmd.aggregateId()).cast(MoneyDeposited.class);
return this.aggregateFacade.executeReturnEvent(cmd, cmd.aggregateId(), MoneyDeposited.class)
.cast(MoneyDeposited.class);
}
@Override
public Mono<MoneyWithdrawn> execute(WithdrawMoney cmd) {
return this.aggregateFacade.executeReturnEvent(cmd, cmd.aggregateId()).cast(MoneyWithdrawn.class);
return this.aggregateFacade.executeReturnEvent(cmd, cmd.aggregateId(), MoneyWithdrawn.class)
.cast(MoneyWithdrawn.class);
}
// private void handleTransactionFailed(ApplicationMessageBus bus) {
// bus.messagesStream()
// .filter(m -> m instanceof TransactionFailed)
// .cast(TransactionFailed.class)
// .flatMap(e -> execute(DepositMoney.builder()
// .correlationId(e.correlationId())
// .aggregateId(e.fromAccountId())
// .amount(e.amount())
// .build()))
// .map(Optional::of)
// .doOnError(e -> log.error("handleTransactionFailed -> ", e))
// .retry()
// .subscribe();
// }
private void handleTransactionCreated(ApplicationMessageBus bus) {
bus.messagesStream()
.filter(m -> m instanceof TransactionCreated)
.cast(TransactionCreated.class)
.flatMap(e -> execute(WithdrawTransferMoney.builder()
.aggregateId(e.fromAccountId())
.transactionId(e.aggregateId())
.correlationId(e.correlationId())
.fromAccount(e.fromAccountId())
.toAccount(e.toAccountId())
.amount(e.amount())
.build(), TransferMoneyWithdrawn.class))
.doOnError(e -> log.error("handleTransactionCreated -> ", e))
.retry()
.subscribe();
}
private void handleTransferMoneyWithdrawn(ApplicationMessageBus bus) {
bus.messagesStream()
.filter(m -> m instanceof TransferMoneyWithdrawn)
.cast(TransferMoneyWithdrawn.class)
.flatMap(e -> execute(DepositTransferMoney.builder()
.aggregateId(e.toAccount())
.transactionId(e.transactionId())
.correlationId(e.correlationId())
.fromAccount(e.fromAccount())
.toAccount(e.toAccount())
.amount(e.amount())
.build(), TransferMoneyDeposited.class))
.doOnError(e -> log.error("handleTransferMoneyWithdrawn -> ", e))
.retry()
.subscribe();
}
}

View File

@@ -0,0 +1,41 @@
package com.mz.reactor.ddd.reactorddd.account.impl;
import com.mz.reactor.ddd.common.components.bus.ApplicationMessageBus;
import com.mz.reactor.ddd.reactorddd.account.api.AccountQuery;
import com.mz.reactor.ddd.reactorddd.account.domain.AccountState;
import com.mz.reactor.ddd.reactorddd.account.wiring.AccountConfiguration;
import com.mz.reactor.ddd.reactorddd.persistance.view.impl.ViewRepository;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Objects;
@Service
public class AccountQueryImpl implements AccountQuery {
private final ViewRepository<AccountState> repository;
public AccountQueryImpl(
@Qualifier(AccountConfiguration.ACCOUNT_VIEW_REPOSITORY) ViewRepository<AccountState> repository,
ApplicationMessageBus bus
) {
this.repository = Objects.requireNonNull(repository);
Objects.requireNonNull(bus).messagesStream()
.filter(m -> m instanceof AccountState)
.cast(AccountState.class)
.subscribe(repository::addView);
}
@Override
public Mono<AccountState> findById(@NonNull String id) {
return repository.findBy(v -> v.id().equals(id));
}
@Override
public Flux<AccountState> getAll() {
return repository.findAllBy(v -> true);
}
}

View File

@@ -2,20 +2,19 @@ package com.mz.reactor.ddd.reactorddd.account.wiring;
import com.mz.reactor.ddd.common.api.valueobject.Id;
import com.mz.reactor.ddd.common.components.bus.ApplicationMessageBus;
import com.mz.reactor.ddd.common.components.bus.impl.ApplicationMessageBusImpl;
import com.mz.reactor.ddd.reactorddd.account.domain.AccountAggregate;
import com.mz.reactor.ddd.reactorddd.account.domain.AccountCommandHandler;
import com.mz.reactor.ddd.reactorddd.account.domain.AccountEventApplier;
import com.mz.reactor.ddd.reactorddd.account.domain.AccountState;
import com.mz.reactor.ddd.reactorddd.account.domain.command.AccountCommand;
import com.mz.reactor.ddd.reactorddd.account.domain.event.AccountEvent;
import com.mz.reactor.ddd.reactorddd.persistance.aggregate.AggregateFacade;
import com.mz.reactor.ddd.reactorddd.persistance.aggregate.AggregateRepository;
import com.mz.reactor.ddd.reactorddd.persistance.aggregate.impl.AggregateFacadeImpl;
import com.mz.reactor.ddd.reactorddd.persistance.aggregate.impl.AggregateRepositoryImpl;
import com.mz.reactor.ddd.reactorddd.persistance.view.impl.ViewRepository;
import com.mz.reactor.ddd.reactorddd.persistance.view.impl.impl.ViewRepositoryImpl;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import java.util.function.Function;
@@ -24,23 +23,31 @@ import java.util.function.Function;
public class AccountConfiguration {
public static final String ACCOUNT_AGGREGATE_REPOSITORY = "accountAggregateRepository";
public static final String ACCOUNT_AGGREGATE_FACADE = "accountAggregateFacade";
public static final String ACCOUNT_VIEW_REPOSITORY = "accountViewRepository";
private final AccountEventApplier accountEventApplier = new AccountEventApplier();
private final AccountCommandHandler accountCommandHandler = new AccountCommandHandler();
private final Function<Id, AccountAggregate> aggregateFactory = id -> new AccountAggregate(id.toString());
private final Function<Id, AccountAggregate> aggregateFactory = id -> new AccountAggregate(id.getValue());
private final Function<AccountAggregate, AccountState> stateFactory = AccountAggregate::getState;
@Bean(ACCOUNT_AGGREGATE_REPOSITORY)
public AggregateRepository<AccountAggregate, AccountCommand, AccountEvent, AccountState> getAggregateRepository() {
public AggregateRepository<AccountAggregate, AccountCommand, AccountState> getAggregateRepository() {
return new AggregateRepositoryImpl<>(accountCommandHandler, accountEventApplier, aggregateFactory, stateFactory);
}
@Bean("accountAggregateFacade")
public AggregateFacade<AccountAggregate, AccountCommand, AccountEvent, AccountState> getAggregateFacade(
@Qualifier(ACCOUNT_AGGREGATE_REPOSITORY) AggregateRepository aggregateRepository,
@Bean(ACCOUNT_AGGREGATE_FACADE)
public AggregateFacade<AccountAggregate, AccountCommand, AccountState> getAggregateFacade(
@Qualifier(ACCOUNT_AGGREGATE_REPOSITORY) AggregateRepository<AccountAggregate, AccountCommand, AccountState> aggregateRepository,
@Qualifier(ACCOUNT_VIEW_REPOSITORY) ViewRepository<AccountState> viewRepository,
ApplicationMessageBus bus
) {
return new AggregateFacadeImpl<>(aggregateRepository, bus::publishMessage, s -> {});
return new AggregateFacadeImpl<>(aggregateRepository, bus::publishMessage, bus::publishMessage);
}
@Bean(ACCOUNT_VIEW_REPOSITORY)
public ViewRepository<AccountState> getViewRepository() {
return new ViewRepositoryImpl<AccountState>();
}
}

View File

@@ -2,12 +2,14 @@ package com.mz.reactor.ddd.reactorddd.application;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import reactor.core.publisher.Hooks;
@SpringBootApplication
public class BankAccountApp {
public static void main(String[] args) {
Hooks.onOperatorDebug();
SpringApplication.run(BankAccountApp.class, args);
}

View File

@@ -2,10 +2,12 @@ package com.mz.reactor.ddd.reactorddd.application;
import com.mz.reactor.ddd.common.components.http.HttpErrorHandler;
import com.mz.reactor.ddd.reactorddd.account.api.AccountHandler;
import com.mz.reactor.ddd.reactorddd.transaction.api.TransactionHandler;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
@@ -14,17 +16,21 @@ import reactor.core.publisher.Mono;
@Configuration
@ComponentScan(basePackages = {"com.mz.reactor.ddd.*"})
@Import({com.mz.reactor.ddd.reactorddd.account.wiring.AccountConfiguration.class})
//@Import({AccountConfiguration.class, TransactionConfiguration.class})
public class BankAccountAppConfiguration {
private static final Log log = LogFactory.getLog(BankAccountAppConfiguration.class);
@Bean
public RouterFunction<ServerResponse> statisticRoute(AccountHandler handler) {
public RouterFunction<ServerResponse> statisticRoute(AccountHandler accountHandler, TransactionHandler transactionHandler) {
return RouterFunctions.route()
.add(handler.route())
.add(accountHandler.route())
.add(transactionHandler.route())
.GET("/health", req -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(Mono.just("Tick"), String.class))
.onError(Throwable.class, HttpErrorHandler.FN::onError)
.onError(Throwable.class,
(throwable, serverRequest) -> HttpErrorHandler.FN.onError(throwable, serverRequest, error -> log.error("Error: ", error)))
.build();
}

View File

@@ -0,0 +1,126 @@
package com.mz.reactor.ddd.reactorddd.application;
import com.mz.reactor.ddd.reactorddd.account.api.model.CreateAccountRequest;
import com.mz.reactor.ddd.reactorddd.account.api.model.CreateAccountResponse;
import com.mz.reactor.ddd.reactorddd.account.domain.AccountState;
import com.mz.reactor.ddd.reactorddd.account.domain.command.CreateAccount;
import com.mz.reactor.ddd.reactorddd.transaction.api.model.CreateTransactionRequest;
import com.mz.reactor.ddd.reactorddd.transaction.api.model.CreateTransactionResponse;
import com.mz.reactor.ddd.reactorddd.transaction.domain.TransactionState;
import com.mz.reactor.ddd.reactorddd.transaction.domain.command.CreateTransaction;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.BodyInserters;
import java.math.BigDecimal;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.registerCustomDateFormat;
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class BankAccountAppTest {
@Autowired
WebTestClient webTestClient;
@Test
public void testApp() {
var correlationId = "testScenarioCreateAccountsAndTransferMoney";
var accountId1 = "account_1";
var accountId2 = "account_2";
createAccount(accountId1, correlationId);
createAccount(accountId2, correlationId);
assertThat(getAllAccounts().size()).isEqualTo(2);
var transactionId = createTransaction(accountId1, accountId2, correlationId, BigDecimal.TEN).payload().aggregateId();
getTransaction(transactionId);
assertThat(getAccount(accountId1).amount().compareTo(BigDecimal.valueOf(90))).isEqualTo(0);
assertThat(getAccount(accountId2).amount().compareTo(BigDecimal.valueOf(110))).isEqualTo(0);
}
private CreateAccountResponse createAccount(String accountId, String correlationId) {
var createAccount1 = CreateAccountRequest.builder()
.payload(CreateAccount.builder()
.aggregateId(accountId)
.balance(BigDecimal.valueOf(100))
.correlationId(correlationId)
.build())
.build();
return webTestClient.post().uri("/accounts")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(createAccount1))
.exchange()
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
.expectBody(CreateAccountResponse.class)
.returnResult().getResponseBody();
}
private List<AccountState> getAllAccounts() {
return webTestClient.get().uri("/accounts")
.accept(MediaType.APPLICATION_JSON_UTF8)
.exchange()
.expectStatus()
.isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
.expectBody(List.class).returnResult().getResponseBody();
}
private CreateTransactionResponse createTransaction(String fromAccount, String toAccount, String correlationId, BigDecimal amount) {
var createTransactionRequest = CreateTransactionRequest.builder()
.payload(CreateTransaction.builder()
.amount(amount)
.correlationId(correlationId)
.fromAccountId(fromAccount)
.toAccountId(toAccount)
.build())
.build();
return webTestClient.post().uri("/transactions")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(createTransactionRequest))
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
.expectBody(CreateTransactionResponse.class).returnResult().getResponseBody();
}
private AccountState getAccount(String id) {
return webTestClient.get().uri("/accounts/{id}", id)
.accept(MediaType.APPLICATION_JSON_UTF8)
.exchange()
.expectStatus().isOk()
// .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
.expectBody(AccountState.class).returnResult().getResponseBody();
}
private String getAccountString(String id) {
return webTestClient.get().uri("/accounts/{id}", id)
.accept(MediaType.APPLICATION_JSON_UTF8)
.exchange()
.expectStatus().isOk()
// .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
.expectBody(String.class).returnResult().getResponseBody();
}
private TransactionState getTransaction(String id) {
return webTestClient.get().uri("/transactions/{id}", id)
.accept(MediaType.APPLICATION_JSON_UTF8)
.exchange()
.expectStatus()
.isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
.expectBody(TransactionState.class).returnResult().getResponseBody();
}
}

View File

@@ -0,0 +1 @@
description('transactions-adapters')

View File

@@ -0,0 +1,73 @@
package com.mz.reactor.ddd.reactorddd.transaction.adapters.account;
import com.mz.reactor.ddd.common.components.bus.ApplicationMessageBus;
import com.mz.reactor.ddd.reactorddd.account.domain.event.TransferMoneyDeposited;
import com.mz.reactor.ddd.reactorddd.account.domain.event.TransferMoneyFailed;
import com.mz.reactor.ddd.reactorddd.transaction.api.TransactionApplicationService;
import com.mz.reactor.ddd.reactorddd.transaction.api.TransactionQuery;
import com.mz.reactor.ddd.reactorddd.transaction.domain.command.CancelTransaction;
import com.mz.reactor.ddd.reactorddd.transaction.domain.command.FinishTransaction;
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionFailed;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Component;
import java.util.Objects;
@Component
public class AccountChangeStreamAdapter {
private static final Log log = LogFactory.getLog(AccountChangeStreamAdapter.class);
private final ApplicationMessageBus messageBus;
private final TransactionApplicationService transactionApplicationService;
private final TransactionQuery query;
public AccountChangeStreamAdapter(
ApplicationMessageBus messageBus,
TransactionApplicationService transactionApplicationService,
TransactionQuery query
) {
this.messageBus = Objects.requireNonNull(messageBus);
this.transactionApplicationService = Objects.requireNonNull(transactionApplicationService);
this.query = Objects.requireNonNull(query);
handleTransferMoneyDeposited(messageBus);
handleTransferMoneyFailed(messageBus);
}
private void handleTransferMoneyDeposited(ApplicationMessageBus bus) {
log.debug("handleTransferMoneyDeposited ->");
bus.messagesStream()
.filter(m -> m instanceof TransferMoneyDeposited)
.cast(TransferMoneyDeposited.class)
.flatMap(e -> query.findById(e.transactionId()))
.flatMap(s -> transactionApplicationService.execute(FinishTransaction.builder()
.aggregateId(s.aggregateId())
.fromAccountId(s.fromAccountId())
.toAccountId(s.toAccountId())
.build()))
.doOnError(error -> log.error("handleMoneyDeposited -> ", error))
.log()
.retry()
.subscribe();
}
private void handleTransferMoneyFailed(ApplicationMessageBus bus) {
log.debug("handleTransferMoneyFailed ->");
bus.messagesStream()
.filter(m -> m instanceof TransferMoneyFailed)
.cast(TransferMoneyFailed.class)
.flatMap(e -> query.findById(e.transactionId()))
.flatMap(s -> transactionApplicationService.execute(CancelTransaction.builder()
.aggregateId(s.aggregateId())
.fromAccountId(s.fromAccountId())
.toAccountId(s.toAccountId())
.build(), TransactionFailed.class))
.doOnError(error -> log.error("handleTransferMoneyFailed -> ", error))
.log()
.retry()
.subscribe();
}
}

View File

@@ -0,0 +1 @@
description('transaction-api module')

View File

@@ -0,0 +1,18 @@
package com.mz.reactor.ddd.reactorddd.transaction.api;
import com.mz.reactor.ddd.common.api.event.DomainEvent;
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.command.TransactionCommand;
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionCreated;
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionFinished;
import reactor.core.publisher.Mono;
public interface TransactionApplicationService {
Mono<TransactionCreated> execute(CreateTransaction createTransaction);
Mono<TransactionFinished> execute(FinishTransaction cmd);
<R extends DomainEvent> Mono<R> execute(TransactionCommand cmd, Class<R> eventType);
}

View File

@@ -0,0 +1,62 @@
package com.mz.reactor.ddd.reactorddd.transaction.api;
import com.mz.reactor.ddd.common.components.http.HttpHandler;
import com.mz.reactor.ddd.reactorddd.transaction.api.model.CreateTransactionRequest;
import com.mz.reactor.ddd.reactorddd.transaction.api.model.CreateTransactionResponse;
import com.mz.reactor.ddd.reactorddd.transaction.domain.TransactionState;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import java.util.Objects;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
@Component
public class TransactionHandler implements HttpHandler {
private final TransactionApplicationService service;
private final TransactionQuery query;
public TransactionHandler(TransactionApplicationService transactionApplicationService, TransactionQuery query) {
this.service = Objects.requireNonNull(transactionApplicationService);
this.query = Objects.requireNonNull(query);
}
public Mono<ServerResponse> getAll(ServerRequest request) {
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(query.getAll(), TransactionState.class);
}
public Mono<ServerResponse> getById(ServerRequest request) {
return query.findById(request.pathVariable("id"))
.flatMap(this::mapToResponse);
}
public Mono<ServerResponse> createTransaction(ServerRequest request) {
return request
.bodyToMono(CreateTransactionRequest.class)
.map(CreateTransactionRequest::payload)
.flatMap(service::execute)
.map(CreateTransactionResponse::from)
.flatMap(this::mapToResponse);
}
@Override
public RouterFunction<ServerResponse> route() {
var route = RouterFunctions
.route(POST("").and(accept(MediaType.APPLICATION_JSON_UTF8)), this::createTransaction)
.andRoute(GET("/").and(accept(MediaType.APPLICATION_JSON_UTF8)), this::getAll)
.andRoute(GET("/{id}").and(accept(MediaType.APPLICATION_JSON_UTF8)), this::getById);
return RouterFunctions.route()
.nest(path("/transactions"), () -> route)
.build();
}
}

View File

@@ -0,0 +1,11 @@
package com.mz.reactor.ddd.reactorddd.transaction.api;
import com.mz.reactor.ddd.reactorddd.transaction.domain.TransactionState;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface TransactionQuery {
Mono<TransactionState> findById(String id);
Flux<TransactionState> getAll();
}

View File

@@ -0,0 +1,18 @@
package com.mz.reactor.ddd.reactorddd.transaction.api.model;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.mz.reactor.ddd.reactorddd.transaction.domain.command.CreateTransaction;
import org.immutables.value.Value;
@Value.Immutable
@JsonSerialize(as = ImmutableCreateTransactionRequest.class)
@JsonDeserialize(as = ImmutableCreateTransactionRequest.class)
public interface CreateTransactionRequest {
CreateTransaction payload();
static ImmutableCreateTransactionRequest.Builder builder() {
return ImmutableCreateTransactionRequest.builder();
}
}

View File

@@ -0,0 +1,24 @@
package com.mz.reactor.ddd.reactorddd.transaction.api.model;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionCreated;
import org.immutables.value.Value;
@Value.Immutable
@JsonSerialize(as = ImmutableCreateTransactionResponse.class)
@JsonDeserialize(as = ImmutableCreateTransactionResponse.class)
public interface CreateTransactionResponse {
TransactionCreated payload();
static CreateTransactionResponse from(TransactionCreated transactionCreated) {
return builder()
.payload(transactionCreated)
.build();
}
static ImmutableCreateTransactionResponse.Builder builder() {
return ImmutableCreateTransactionResponse.builder();
}
}

View File

@@ -0,0 +1,33 @@
package com.mz.reactor.ddd.reactorddd.transaction.domain;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.mz.reactor.ddd.common.api.view.DomainView;
import org.immutables.value.Value;
import java.math.BigDecimal;
@Value.Immutable
@JsonSerialize(as = ImmutableTransactionState.class)
@JsonDeserialize(as = ImmutableTransactionState.class)
public interface TransactionState extends DomainView {
default String id() {
return aggregateId();
}
String aggregateId();
String fromAccountId();
String toAccountId();
BigDecimal amount();
TransactionStatus status();
static ImmutableTransactionState.Builder builder() {
return ImmutableTransactionState.builder();
}
}

View File

@@ -0,0 +1,22 @@
package com.mz.reactor.ddd.reactorddd.transaction.domain.command;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.immutables.value.Value;
import java.util.concurrent.TransferQueue;
@Value.Immutable
@JsonSerialize(as = ImmutableCancelTransaction.class)
@JsonDeserialize(as = ImmutableCancelTransaction.class)
public interface CancelTransaction extends TransactionCommand {
String fromAccountId();
String toAccountId();
static ImmutableCancelTransaction.Builder builder() {
return ImmutableCancelTransaction.builder();
}
}

View File

@@ -1,12 +1,22 @@
package com.mz.reactor.ddd.reactorddd.transaction.domain.command;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.immutables.value.Value;
import java.math.BigDecimal;
import java.util.UUID;
@Value.Immutable
@JsonSerialize(as = ImmutableCreateTransaction.class)
@JsonDeserialize(as = ImmutableCreateTransaction.class)
public interface CreateTransaction extends TransactionCommand {
@Value.Default
default String aggregateId() {
return UUID.randomUUID().toString();
}
String fromAccountId();
String toAccountId();

View File

@@ -1,8 +1,12 @@
package com.mz.reactor.ddd.reactorddd.transaction.domain.command;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.immutables.value.Value;
@Value.Immutable
@JsonSerialize(as = ImmutableFinishTransaction.class)
@JsonDeserialize(as = ImmutableFinishTransaction.class)
public interface FinishTransaction extends TransactionCommand {
String fromAccountId();

View File

@@ -1,10 +1,14 @@
package com.mz.reactor.ddd.reactorddd.transaction.domain.event;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.immutables.value.Value;
import java.math.BigDecimal;
@Value.Immutable
@JsonSerialize(as = ImmutableTransactionCreated.class)
@JsonDeserialize(as = ImmutableTransactionCreated.class)
public interface TransactionCreated extends TransactionEvent {
String fromAccountId();

View File

@@ -1,11 +1,16 @@
package com.mz.reactor.ddd.reactorddd.transaction.domain.event;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.mz.reactor.ddd.reactorddd.transaction.domain.command.CancelTransaction;
import com.mz.reactor.ddd.reactorddd.transaction.domain.command.CreateTransaction;
import org.immutables.value.Value;
import java.math.BigDecimal;
@Value.Immutable
@JsonSerialize(as = ImmutableTransactionFailed.class)
@JsonDeserialize(as = ImmutableTransactionFailed.class)
public interface TransactionFailed extends TransactionEvent {
String fromAccountId();

View File

@@ -1,6 +1,7 @@
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.CancelTransaction;
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;
@@ -8,6 +9,7 @@ import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionFailed;
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionFinished;
import java.math.BigDecimal;
import java.util.stream.Collectors;
public class TransactionAggregate {
@@ -59,6 +61,20 @@ public class TransactionAggregate {
}
}
public TransactionFailed validateCancelTransaction(CancelTransaction command) {
if (status == TransactionStatus.CREATED) {
return TransactionFailed.builder()
.aggregateId(aggregateId.getValue())
.correlationId(command.correlationId())
.fromAccountId(fromAccount.getValue())
.toAccountId(toAccount.getValue())
.amount(this.amount)
.build();
} else {
throw new RuntimeException(String.format("Transaction in the state: %s can't be canceled!", status));
}
}
public TransactionAggregate applyTransactionCreated(TransactionCreated created) {
this.fromAccount = new Id(created.fromAccountId());
this.toAccount = new Id(created.toAccountId());
@@ -77,7 +93,7 @@ public class TransactionAggregate {
return this;
}
public TransactionState getStatus() {
public TransactionState getState() {
return TransactionState.builder()
.amount(amount)
.fromAccountId(fromAccount.getValue())

View File

@@ -5,6 +5,7 @@ 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.command.CancelTransaction;
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.command.TransactionCommand;
@@ -21,13 +22,54 @@ public class TransactionCommandHandler implements CommandHandler<TransactionAggr
public TransactionCommandHandler() {
}
@Override
public CommandResult execute(TransactionAggregate aggregate, TransactionCommand command) {
if (command instanceof CreateTransaction) {
return doCreateTransaction(aggregate, (CreateTransaction) command);
} else if (command instanceof FinishTransaction) {
return doFinishTransaction(aggregate, (FinishTransaction) command);
} else if (command instanceof CancelTransaction) {
return doCancelTransaction(aggregate, (CancelTransaction) command);
} else {
return CommandResult.badCommand(command);
}
}
private CommandResult doCancelTransaction(TransactionAggregate aggregate, CancelTransaction command) {
try {
return Optional.of(command)
.map(aggregate::validateCancelTransaction)
.map(e -> CommandResult.builder()
.commandId(command.commandId())
.events(List.of(e))
.statusCode(CommandResult.StatusCode.OK)
.build())
.orElseGet(() -> (ImmutableCommandResult) CommandResult.notModified(command));
} catch (RuntimeException e) {
return CommandResult.builder()
.commandId(Optional.ofNullable(command)
.map(Command::commandId)
.orElseGet(() -> UUID.randomUUID().toString()))
.events(List.of(TransactionFailed.builder()
.aggregateId(command.aggregateId())
.correlationId(command.correlationId())
.fromAccountId(command.fromAccountId())
.toAccountId(command.toAccountId())
.amount(aggregate.getState().amount())
.build()))
.statusCode(CommandResult.StatusCode.FAILED)
.error(e)
.build();
}
}
private CommandResult doCreateTransaction(TransactionAggregate aggregate, CreateTransaction command) {
try {
return Optional.of(command)
.map(aggregate::validateCreateTransaction)
.map(e -> CommandResult.builder()
.commandId(command.commandId())
.addEvents(e)
.events(List.of(e))
.statusCode(CommandResult.StatusCode.OK)
.build())
.orElseGet(() -> (ImmutableCommandResult) CommandResult.notModified(command));
@@ -47,7 +89,13 @@ public class TransactionCommandHandler implements CommandHandler<TransactionAggr
try {
return Optional.of(command)
.map(aggregate::validateFinishTransaction)
.map(e -> CommandResult.builder().build())
.map(e -> CommandResult.builder()
.commandId(Optional.ofNullable(command)
.map(Command::commandId)
.orElseGet(() -> UUID.randomUUID().toString()))
.events(List.of(e))
.statusCode(CommandResult.StatusCode.OK)
.build())
.orElseGet(() -> (ImmutableCommandResult) CommandResult.notModified(command));
} catch (RuntimeException e) {
return CommandResult.builder()
@@ -56,25 +104,13 @@ public class TransactionCommandHandler implements CommandHandler<TransactionAggr
.orElseGet(() -> UUID.randomUUID().toString()))
.statusCode(CommandResult.StatusCode.FAILED)
.error(e)
.addEvents(FinishTransactionFailed.builder()
.events(List.of(FinishTransactionFailed.builder()
.aggregateId(command.aggregateId())
.correlationId(command.correlationId())
.fromAccountId(command.fromAccountId())
.toAccountId(command.toAccountId())
.build())
.build()))
.build();
}
}
@Override
public CommandResult execute(TransactionAggregate aggregate, TransactionCommand command) {
if (command instanceof CreateTransaction) {
return doCreateTransaction(aggregate, (CreateTransaction) command);
}
if (command instanceof FinishTransaction) {
return doFinishTransaction(aggregate, (FinishTransaction) command);
}else {
return CommandResult.badCommand(command);
}
}
}

View File

@@ -1,5 +1,6 @@
package com.mz.reactor.ddd.reactorddd.transaction.domain;
import com.mz.reactor.ddd.common.api.event.DomainEvent;
import com.mz.reactor.ddd.common.api.event.EventApplier;
import com.mz.reactor.ddd.reactorddd.transaction.domain.TransactionAggregate;
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionCreated;
@@ -7,11 +8,11 @@ import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionEvent;
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionFailed;
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionFinished;
public class TransactionEventApplier implements EventApplier<TransactionAggregate, TransactionEvent> {
public class TransactionEventApplier implements EventApplier<TransactionAggregate> {
@Override
public TransactionAggregate apply(TransactionAggregate aggregate, TransactionEvent event) {
public <E extends DomainEvent> TransactionAggregate apply(TransactionAggregate aggregate, E event) {
if (event instanceof TransactionCreated) {
return applyTransactionCreated(aggregate, (TransactionCreated) event);
}

View File

@@ -1,24 +0,0 @@
package com.mz.reactor.ddd.reactorddd.transaction.domain;
import org.immutables.value.Value;
import java.math.BigDecimal;
@Value.Immutable
public interface TransactionState {
String aggregateId();
String fromAccountId();
String toAccountId();
BigDecimal amount();
TransactionStatus status();
static ImmutableTransactionState.Builder builder() {
return ImmutableTransactionState.builder();
}
}

View File

@@ -69,7 +69,7 @@ class TransactionAggregateTest {
.build();
var aggregate = new TransactionAggregate(aggregateId);
var state = aggregate.applyTransactionCreated(event).getStatus();
var state = aggregate.applyTransactionCreated(event).getState();
assertEquals(state.aggregateId(), aggregateId);
assertEquals(state.toAccountId(), toAccountId);
@@ -149,7 +149,7 @@ class TransactionAggregateTest {
.fromAccountId(fromAccountId)
.aggregateId(aggregateId)
.build();
var state = aggregate.applyTransactionFinished(transactionFinished).getStatus();
var state = aggregate.applyTransactionFinished(transactionFinished).getState();
//then
assertEquals(state.status(), TransactionStatus.FINISHED);
@@ -181,7 +181,7 @@ class TransactionAggregateTest {
.aggregateId(aggregateId)
.amount(BigDecimal.TEN)
.build();
var state = aggregate.applyTransactionFailed(transactionFailed).getStatus();
var state = aggregate.applyTransactionFailed(transactionFailed).getState();
//then
assertEquals(state.status(), TransactionStatus.FAILED);

View File

@@ -30,7 +30,7 @@ class TransactionEventApplierTest {
var aggregate = new TransactionAggregate(aggregateId);
//when
var status = subject.apply(aggregate, transactionCreated).getStatus();
var status = subject.apply(aggregate, transactionCreated).getState();
//then
Assertions.assertEquals(status.amount(), BigDecimal.TEN);
@@ -61,7 +61,7 @@ class TransactionEventApplierTest {
subject.apply(aggregate, transactionCreated);
//when
var status = subject.apply(aggregate, transactionFinished).getStatus();
var status = subject.apply(aggregate, transactionFinished).getState();
//then
Assertions.assertEquals(status.amount(), BigDecimal.TEN);
@@ -93,7 +93,7 @@ class TransactionEventApplierTest {
subject.apply(aggregate, transactionCreated);
//when
var status = subject.apply(aggregate, transactionFailed).getStatus();
var status = subject.apply(aggregate, transactionFailed).getState();
//then
Assertions.assertEquals(status.amount(), BigDecimal.TEN);

View File

@@ -0,0 +1 @@
description('transaction-impl module')

View File

@@ -0,0 +1,60 @@
package com.mz.reactor.ddd.reactorddd.transaction.impl;
import com.mz.reactor.ddd.common.api.event.DomainEvent;
import com.mz.reactor.ddd.common.components.bus.ApplicationMessageBus;
import com.mz.reactor.ddd.reactorddd.account.domain.command.AccountCommand;
import com.mz.reactor.ddd.reactorddd.account.domain.event.MoneyDeposited;
import com.mz.reactor.ddd.reactorddd.persistance.aggregate.AggregateFacade;
import com.mz.reactor.ddd.reactorddd.transaction.api.TransactionApplicationService;
import com.mz.reactor.ddd.reactorddd.transaction.api.TransactionQuery;
import com.mz.reactor.ddd.reactorddd.transaction.domain.TransactionAggregate;
import com.mz.reactor.ddd.reactorddd.transaction.domain.TransactionState;
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.command.TransactionCommand;
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionCreated;
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionFinished;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import java.util.Objects;
import java.util.UUID;
import static com.mz.reactor.ddd.reactorddd.transaction.wiring.TransactionConfiguration.TRANSACTION_AGGREGATE_FACADE;
@Service
public class TransactionApplicationServiceImpl implements TransactionApplicationService {
private static final Log log = LogFactory.getLog(TransactionApplicationServiceImpl.class);
private final AggregateFacade<TransactionAggregate, TransactionCommand, TransactionState> aggregateFacade;
public TransactionApplicationServiceImpl(
@Qualifier(TRANSACTION_AGGREGATE_FACADE) AggregateFacade<TransactionAggregate, TransactionCommand, TransactionState> aggregateFacade,
ApplicationMessageBus bus,
TransactionQuery query
) {
this.aggregateFacade = Objects.requireNonNull(aggregateFacade);
}
@Override
public Mono<TransactionCreated> execute(CreateTransaction createTransaction) {
return aggregateFacade.executeReturnEvent(createTransaction, UUID.randomUUID().toString(), TransactionCreated.class)
.cast(TransactionCreated.class);
}
@Override
public Mono<TransactionFinished> execute(FinishTransaction cmd) {
return aggregateFacade.executeReturnEvent(cmd, cmd.aggregateId(), TransactionFinished.class)
.cast(TransactionFinished.class);
}
@Override
public <R extends DomainEvent> Mono<R> execute(TransactionCommand cmd, Class<R> eventType) {
return this.aggregateFacade.executeReturnEvent(cmd, cmd.aggregateId(), eventType)
.cast(eventType);
}
}

View File

@@ -0,0 +1,42 @@
package com.mz.reactor.ddd.reactorddd.transaction.impl;
import com.mz.reactor.ddd.common.components.bus.ApplicationMessageBus;
import com.mz.reactor.ddd.reactorddd.persistance.view.impl.ViewRepository;
import com.mz.reactor.ddd.reactorddd.transaction.api.TransactionQuery;
import com.mz.reactor.ddd.reactorddd.transaction.domain.TransactionState;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Objects;
import static com.mz.reactor.ddd.reactorddd.transaction.wiring.TransactionConfiguration.TRANSACTION_VIEW_REPOSITORY;
@Service
public class TransactionQueryImpl implements TransactionQuery {
private final ViewRepository<TransactionState> repository;
public TransactionQueryImpl(
@Qualifier(TRANSACTION_VIEW_REPOSITORY) ViewRepository<TransactionState> repository,
ApplicationMessageBus bus
) {
this.repository = Objects.requireNonNull(repository);
Objects.requireNonNull(bus).messagesStream()
.filter(m -> m instanceof TransactionState)
.cast(TransactionState.class)
.subscribe(repository::addView);
}
@Override
public Mono<TransactionState> findById(@NonNull String id) {
return repository.findBy(v -> v.id().equals(id));
}
@Override
public Flux<TransactionState> getAll() {
return repository.findAllBy(v -> true);
}
}

View File

@@ -0,0 +1,52 @@
package com.mz.reactor.ddd.reactorddd.transaction.wiring;
import com.mz.reactor.ddd.common.api.valueobject.Id;
import com.mz.reactor.ddd.common.components.bus.ApplicationMessageBus;
import com.mz.reactor.ddd.reactorddd.persistance.aggregate.AggregateFacade;
import com.mz.reactor.ddd.reactorddd.persistance.aggregate.AggregateRepository;
import com.mz.reactor.ddd.reactorddd.persistance.aggregate.impl.AggregateFacadeImpl;
import com.mz.reactor.ddd.reactorddd.persistance.aggregate.impl.AggregateRepositoryImpl;
import com.mz.reactor.ddd.reactorddd.persistance.view.impl.ViewRepository;
import com.mz.reactor.ddd.reactorddd.persistance.view.impl.impl.ViewRepositoryImpl;
import com.mz.reactor.ddd.reactorddd.transaction.domain.TransactionAggregate;
import com.mz.reactor.ddd.reactorddd.transaction.domain.TransactionCommandHandler;
import com.mz.reactor.ddd.reactorddd.transaction.domain.TransactionEventApplier;
import com.mz.reactor.ddd.reactorddd.transaction.domain.TransactionState;
import com.mz.reactor.ddd.reactorddd.transaction.domain.command.TransactionCommand;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.function.Function;
@Configuration
public class TransactionConfiguration {
public static final String TRANSACTION_AGGREGATE_REPOSITORY = "transactionAggregateRepository";
public static final String TRANSACTION_AGGREGATE_FACADE = "transactionAggregateFacade";
public static final String TRANSACTION_VIEW_REPOSITORY = "transactionViewRepository";
private final TransactionEventApplier transactionEventApplier = new TransactionEventApplier();
private final TransactionCommandHandler transactionCommandHandler = new TransactionCommandHandler();
private final Function<Id, TransactionAggregate> aggregateFactory = id -> new TransactionAggregate(id.getValue());
private final Function<TransactionAggregate, TransactionState> stateFactory = TransactionAggregate::getState;
@Bean(TRANSACTION_AGGREGATE_REPOSITORY)
public AggregateRepository<TransactionAggregate, TransactionCommand, TransactionState> getAggregateRepository() {
return new AggregateRepositoryImpl<>(transactionCommandHandler, transactionEventApplier, aggregateFactory, stateFactory);
}
@Bean(TRANSACTION_AGGREGATE_FACADE)
public AggregateFacade<TransactionAggregate, TransactionCommand, TransactionState> getAggregateFacade(
@Qualifier(TRANSACTION_AGGREGATE_REPOSITORY) AggregateRepository<TransactionAggregate, TransactionCommand, TransactionState> aggregateRepository,
@Qualifier(TRANSACTION_VIEW_REPOSITORY) ViewRepository<TransactionState> viewRepository,
ApplicationMessageBus bus
) {
return new AggregateFacadeImpl<>(aggregateRepository, bus::publishMessage, bus::publishMessage);
}
@Bean(TRANSACTION_VIEW_REPOSITORY)
public ViewRepository<TransactionState> getTransactionViewRepository() {
return new ViewRepositoryImpl<>();
}
}

View File

@@ -16,7 +16,6 @@ buildscript {
}
plugins {
// id 'org.springframework.boot' version "$springBootVersion"
id 'io.spring.dependency-management' version "$springDependencyMavagementVersion"
id 'java'
id 'java-library'
@@ -48,7 +47,6 @@ subprojects {
sourceCompatibility = '11'
dependencies {
// https://mvnrepository.com/artifact/com.google.guava/guava
implementation group: 'com.google.guava', name: 'guava', version: '28.1-jre'
annotationProcessor 'org.immutables:value:2.7.5'
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-engine'
@@ -73,10 +71,6 @@ project(':bank-account:bank-account-application') {
apply plugin: 'io.spring.dependency-management'
apply plugin: 'org.springframework.boot'
// springBoot {
// mainClassName = 'com.mz.reactor.ddd.reactorddd.application.BankAccountApp'
// }
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation project(':common-api')
@@ -85,7 +79,11 @@ project(':bank-account:bank-account-application') {
implementation project(':bank-account:account-impl')
implementation project(':bank-account:account-api')
implementation project(':bank-account:account-domain')
implementation project(':bank-account:account-adapters')
implementation project(':bank-account:transaction-impl')
implementation project(':bank-account:transaction-api')
implementation project(':bank-account:transaction-domain')
implementation project(':bank-account:transaction-adapters')
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.projectreactor:reactor-test'
}
@@ -108,20 +106,55 @@ project(':bank-account:account-api') {
dependencies {
api project(':common-api')
api project(':common-components')
api project(':shared-spring-dependecies')
api project(':shared-spring-dependencies')
api project(':bank-account:account-domain-api')
}
}
project(':bank-account:transaction-api') {
dependencies {
api project(':common-api')
api project(':common-components')
api project(':shared-spring-dependencies')
api project(':bank-account:transaction-domain-api')
}
}
project(':bank-account:account-impl') {
dependencies {
api project(':common-api')
api project(':common-components')
api project(':common-persistence')
api project(':shared-spring-dependecies')
implementation project(':bank-account:account-domain-api')
implementation project(':bank-account:account-api')
implementation project(':bank-account:account-domain')
api project(':shared-spring-dependencies')
api project(':shared-spring-dependencies')
api project(':bank-account:transaction-domain-api')
api project(':bank-account:account-domain-api')
api project(':bank-account:account-api')
api project(':bank-account:account-domain')
}
}
project(':bank-account:transaction-adapters') {
dependencies {
api project(':common-api')
api project(':bank-account:transaction-domain-api')
api project(':bank-account:transaction-api')
api project(':bank-account:account-domain-api')
api project(':shared-spring-dependencies')
}
}
project(':bank-account:transaction-impl') {
dependencies {
api project(':common-api')
api project(':common-components')
api project(':common-persistence')
api project(':shared-spring-dependencies')
api project(':bank-account:transaction-domain-api')
api project(':bank-account:account-domain-api')
api project(':bank-account:transaction-api')
api project(':bank-account:transaction-domain')
}
}
@@ -132,6 +165,16 @@ project(':bank-account:transaction-domain') {
}
}
project(':bank-account:account-adapters') {
dependencies {
api project(':common-api')
api project(':bank-account:transaction-domain-api')
api project(':bank-account:account-api')
api project(':bank-account:account-domain-api')
api project(':shared-spring-dependencies')
}
}
project(':bank-account:transaction-domain-api') {
dependencies {
api project(':common-api')
@@ -141,12 +184,13 @@ project(':bank-account:transaction-domain-api') {
project(':shared-dependencies') {
dependencies {
api 'org.immutables:value:2.7.5'
api 'com.fasterxml.jackson.core:jackson-databind:2.9.8'
api 'com.fasterxml.jackson.core:jackson-databind:2.10.1'
api 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.10.1'
api group: 'org.apache.commons', name: 'commons-lang3', version: '3.9'
}
}
project(':shared-spring-dependecies') {
project(':shared-spring-dependencies') {
apply plugin: 'io.spring.dependency-management'
dependencies {
@@ -161,7 +205,8 @@ project(':common-persistence') {
dependencies {
api project(':common-api')
implementation group: 'io.projectreactor', name: 'reactor-core'
// implementation group: 'io.projectreactor', name: 'reactor-core'
api project(':shared-spring-dependencies')
testImplementation 'io.projectreactor:reactor-test'
}
}
@@ -171,7 +216,7 @@ project(':common-components') {
dependencies {
api project(':common-api')
api project(':shared-spring-dependecies')
api project(':shared-spring-dependencies')
testImplementation 'io.projectreactor:reactor-test'
}
}

View File

@@ -1,5 +1,6 @@
package com.mz.reactor.ddd.common.api;
import com.fasterxml.jackson.annotation.JsonInclude;
import org.immutables.value.Value;
import java.time.Instant;
@@ -7,6 +8,7 @@ import java.util.Optional;
public interface Message {
@JsonInclude(JsonInclude.Include.NON_EMPTY)
Optional<String> correlationId();
@Value.Default

View File

@@ -5,6 +5,6 @@ import com.mz.reactor.ddd.common.api.event.DomainEvent;
@FunctionalInterface
public interface CommandHandler<A, C extends Command> {
<E extends DomainEvent> CommandResult<E> execute(A aggregate, C command);
CommandResult execute(A aggregate, C command);
}

View File

@@ -8,20 +8,20 @@ import java.util.Optional;
import java.util.UUID;
@Value.Immutable
public interface CommandResult<E extends DomainEvent> {
public interface CommandResult {
enum StatusCode {
OK,
BAD_COMMAND,
FAILED,
NOT_MODIFIED;
NOT_MODIFIED
}
String commandId();
StatusCode statusCode();
List<E> events();
List<? extends DomainEvent> events();
Optional<RuntimeException> error();
@@ -29,23 +29,24 @@ public interface CommandResult<E extends DomainEvent> {
return ImmutableCommandResult.builder();
}
static <D extends DomainEvent> CommandResult<D> fromError(RuntimeException error, D event, Command command) {
static CommandResult fromError(RuntimeException error, List<? extends DomainEvent> events, Command command) {
return builder()
.commandId(Optional.ofNullable(command)
.map(Command::commandId)
.orElseGet(() -> UUID.randomUUID().toString()))
.statusCode(StatusCode.BAD_COMMAND)
.events(Optional.ofNullable(event).map(List::of).orElseGet(List::of))
.statusCode(StatusCode.FAILED)
.events(events)
.error(error)
.build();
}
static <D extends DomainEvent> CommandResult<D> badCommand(Command cmd) {
static CommandResult badCommand(Command cmd) {
return builder()
.commandId(Optional.ofNullable(cmd)
.map(Command::commandId)
.orElseGet(() -> UUID.randomUUID().toString()))
.statusCode(StatusCode.BAD_COMMAND)
.events(List.of())
.build();
}

View File

@@ -1,8 +1,8 @@
package com.mz.reactor.ddd.common.api.event;
@FunctionalInterface
public interface EventApplier<A, E extends DomainEvent> {
public interface EventApplier<A> {
A apply(A aggregate, E event);
<E extends DomainEvent> A apply(A aggregate, E event);
}

View File

@@ -5,10 +5,7 @@ public class Id extends StringValue {
super(value);
}
@Override
public String toString() {
return "Id{" +
"value='" + value + '\'' +
'}';
public static Id of(String id) {
return new Id(id);
}
}

View File

@@ -1,5 +1,9 @@
package com.mz.reactor.ddd.common.api.view;
public interface DomainView {
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.mz.reactor.ddd.common.api.event.DomainEvent;
public interface DomainView extends DomainEvent {
@JsonIgnore
String id();
}

View File

@@ -2,8 +2,11 @@ package com.mz.reactor.ddd.common.components.bus.impl;
import com.mz.reactor.ddd.common.api.Message;
import com.mz.reactor.ddd.common.components.bus.ApplicationMessageBus;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import reactor.core.publisher.DirectProcessor;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;
import reactor.core.publisher.ReplayProcessor;
@@ -14,17 +17,21 @@ import java.util.Optional;
@Service
public class ApplicationMessageBusImpl implements ApplicationMessageBus {
private final ReplayProcessor<Message> messages = ReplayProcessor.create(1);
private static final Log log = LogFactory.getLog(ApplicationMessageBusImpl.class);
private final DirectProcessor<Message> messages = DirectProcessor.create();
private final FluxSink<Message> messagesSink = messages.sink();
@Override
public <M extends Message> void publishMessage(M message) {
log.info(String.format("publishMessage -> messageBusId: %s, message: %s",this.hashCode(), message));
Optional.ofNullable(message).ifPresent(messagesSink::next);
}
@Override
public Flux<Message> messagesStream() {
log.info("messagesStream -> " + this.hashCode());
return messages.publishOn(Schedulers.parallel());
}

View File

@@ -6,12 +6,15 @@ import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import java.util.function.Consumer;
import static org.springframework.web.reactive.function.BodyInserters.fromObject;
public enum HttpErrorHandler {
FN;
public <E extends Throwable> Mono<ServerResponse> onError(E e, ServerRequest req) {
public <E extends Throwable> Mono<ServerResponse> onError(E e, ServerRequest req, Consumer<E> logger) {
logger.accept(e);
return Mono.just(ErrorMessage.builder().error(e.getMessage()).build())
.flatMap(error -> ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
.contentType(MediaType.APPLICATION_JSON_UTF8)

View File

@@ -1,6 +1,7 @@
package com.mz.reactor.ddd.common.components.http;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
@@ -8,9 +9,12 @@ import static org.springframework.web.reactive.function.BodyInserters.fromObject
public interface HttpHandler {
RouterFunction<ServerResponse> route();
default <T> Mono<ServerResponse> mapToResponse(T result) {
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON_UTF8).body(fromObject(result));
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(fromObject(result));
}
}

View File

@@ -7,12 +7,12 @@ import reactor.core.publisher.Mono;
import java.util.function.Function;
public interface AggregateActor<A, C extends Command, E extends DomainEvent> {
public interface AggregateActor<A, C extends Command> {
<S> Mono<S> getState(Function<A, S> stateFactory);
void onDestroy();
Mono<CommandResult<E>> execute(C cmd);
Mono<CommandResult> execute(C cmd);
}

View File

@@ -4,9 +4,9 @@ import com.mz.reactor.ddd.common.api.command.Command;
import com.mz.reactor.ddd.common.api.event.DomainEvent;
import reactor.core.publisher.Mono;
public interface AggregateFacade<A, C extends Command, E extends DomainEvent,S> {
public interface AggregateFacade<A, C extends Command,S> {
Mono<S> execute(C command, String aggregateID);
Mono<E> executeReturnEvent(C command, String aggregateID);
Mono<? extends DomainEvent> executeReturnEvent(C command, String aggregateID, Class<? extends DomainEvent> event);
}

View File

@@ -6,9 +6,9 @@ import com.mz.reactor.ddd.common.api.event.DomainEvent;
import com.mz.reactor.ddd.common.api.valueobject.Id;
import reactor.core.publisher.Mono;
public interface AggregateRepository<A, C extends Command, E extends DomainEvent,S> {
public interface AggregateRepository<A, C extends Command,S> {
Mono<CommandResult<E>> execute(C cmd, Id aggregateId);
Mono<CommandResult> execute(C cmd, Id aggregateId);
Mono<S> findById(Id id);
}

View File

@@ -7,6 +7,8 @@ import com.mz.reactor.ddd.common.api.event.DomainEvent;
import com.mz.reactor.ddd.common.api.event.EventApplier;
import com.mz.reactor.ddd.common.api.valueobject.Id;
import com.mz.reactor.ddd.reactorddd.persistance.aggregate.AggregateActor;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.FluxSink;
import reactor.core.publisher.Mono;
import reactor.core.publisher.ReplayProcessor;
@@ -19,15 +21,17 @@ import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Function;
public class AggregateActorImpl<A, C extends Command, E extends DomainEvent> implements AggregateActor<A, C, E> {
public class AggregateActorImpl<A, C extends Command> implements AggregateActor<A, C> {
private static final Log log = LogFactory.getLog(AggregateActorImpl.class);
private final Id id;
private final CommandHandler<A, C> commandHandler;
private final EventApplier<A, E> eventApplier;
private final EventApplier<A> eventApplier;
private final BiFunction<Id, List<E>, List<E>> persistAll;
private final BiFunction<Id, List<? extends DomainEvent>, List<? extends DomainEvent>> persistAll;
private A aggregate;
@@ -35,17 +39,17 @@ public class AggregateActorImpl<A, C extends Command, E extends DomainEvent> imp
private final FluxSink<C> commandSink = commandProcessor.sink();
private final ReplayProcessor<CommandResult<E>> commandResultReplayProcessor = ReplayProcessor.create();
private final ReplayProcessor<CommandResult> commandResultReplayProcessor = ReplayProcessor.create();
private final FluxSink<CommandResult<E>> commandResultSink = commandResultReplayProcessor.sink();
private final FluxSink<CommandResult> commandResultSink = commandResultReplayProcessor.sink();
public AggregateActorImpl(
Id id,
CommandHandler<A, C> commandHandler,
EventApplier<A, E> eventApplier,
EventApplier<A> eventApplier,
Function<Id, A> aggregateFactory,
List<E> domainEvents,
BiFunction<Id, List<E>, List<E>> persistAll
List<? extends DomainEvent> domainEvents,
BiFunction<Id, List<? extends DomainEvent>, List<? extends DomainEvent>> persistAll
) {
this.id = id;
this.commandHandler = commandHandler;
@@ -60,11 +64,12 @@ public class AggregateActorImpl<A, C extends Command, E extends DomainEvent> imp
commandProcessor
.publishOn(Schedulers.newSingle(String.format("AggregateActor: %s", id)))
.log()
.doOnError(error -> log.error("handleCommand -> ", error))
.subscribe(this::handleCommand);
}
private void handleCommand(C cmd) {
var commandResult = commandHandler.<E>execute(this.aggregate, cmd);
var commandResult = commandHandler.execute(this.aggregate, cmd);
this.aggregate = (A) persistAll
.andThen(events -> events.stream()
.reduce(this.aggregate, eventApplier::apply, (a1, a2) -> a2))
@@ -73,8 +78,8 @@ public class AggregateActorImpl<A, C extends Command, E extends DomainEvent> imp
}
@Override
public Mono<CommandResult<E>> execute(C cmd) {
Mono<CommandResult<E>> result = commandResultReplayProcessor.publishOn(Schedulers.elastic())
public Mono<CommandResult> execute(C cmd) {
Mono<CommandResult> result = commandResultReplayProcessor.publishOn(Schedulers.elastic())
.filter(r -> r.commandId().equals(cmd.commandId()))
.next();
commandSink.next(cmd);

View File

@@ -7,20 +7,25 @@ import com.mz.reactor.ddd.common.api.event.DomainEvent;
import com.mz.reactor.ddd.common.api.valueobject.Id;
import com.mz.reactor.ddd.reactorddd.persistance.aggregate.AggregateFacade;
import com.mz.reactor.ddd.reactorddd.persistance.aggregate.AggregateRepository;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
public class AggregateFacadeImpl<A, C extends Command, E extends DomainEvent ,S> implements AggregateFacade<A, C, E, S> {
public class AggregateFacadeImpl<A, C extends Command, S> implements AggregateFacade<A, C, S> {
private final AggregateRepository<A, C, E, S> aggregateRepository;
private static final Log log = LogFactory.getLog(AggregateFacadeImpl.class);
private final AggregateRepository<A, C, S> aggregateRepository;
private final Consumer<Message> publishChanged;
private final Consumer<S> publishDocument;
public AggregateFacadeImpl(
AggregateRepository<A, C, E, S> aggregateRepository,
AggregateRepository<A, C, S> aggregateRepository,
Consumer<Message> publishChanged,
Consumer<S> publishDocument
) {
@@ -32,29 +37,60 @@ public class AggregateFacadeImpl<A, C extends Command, E extends DomainEvent ,S>
@Override
public Mono<S> execute(C command, String aggregateID) {
return aggregateRepository.execute(command, new Id(aggregateID))
.flatMap(processResult(aggregateID));
.flatMap(cr -> processResult(aggregateID, cr))
.doOnError(error -> log.error("execute -> ", error));
}
@Override
public Mono<E> executeReturnEvent(C command, String aggregateID) {
public Mono<? extends DomainEvent> executeReturnEvent(C command, String aggregateID, Class<? extends DomainEvent> eventType) {
var result = aggregateRepository.execute(command, new Id(aggregateID));
return result.map(r -> r.events().stream().findAny().get());
return result.flatMap(cr -> processResult(aggregateID, eventType, cr))
.doOnError(error -> log.error("execute -> event type: "+eventType, error));
}
private Function<CommandResult<E>, Mono<S>> processResult(String aggregateId) {
return result -> {
switch (result.statusCode()) {
case OK:
return aggregateRepository.findById(new Id(aggregateId))
.doOnSuccess(publishDocument)
.doOnSuccess(s -> result.events().forEach(publishChanged));
case FAILED:
return Mono.error(result.error().orElseGet(() -> new RuntimeException("Generic error")));
case NOT_MODIFIED:
default:
return Mono.empty();
}
};
private Mono<? extends DomainEvent> processResult(String aggregateId, Class<? extends DomainEvent> eventType, CommandResult result) {
switch (result.statusCode()) {
case OK:
return publishChanges(aggregateId, result)
.map(state -> result.events().stream()
.filter(e -> isInstance(e, eventType))
.map(eventType::cast)
.findAny())
.map(Optional::get);
case FAILED:
return onFailed(result);
case NOT_MODIFIED:
default:
return Mono.empty();
}
}
private Mono<S> processResult(String aggregateId, CommandResult result) {
switch (result.statusCode()) {
case OK:
return publishChanges(aggregateId, result);
case FAILED:
return onFailed(result);
case NOT_MODIFIED:
default:
return Mono.empty();
}
}
private <T> Mono<T> onFailed(CommandResult result) {
result.events().forEach(publishChanged);
return Mono.error(result.error().orElseGet(() -> new RuntimeException("Generic error")));
}
private Mono<S> publishChanges(String aggregateId, CommandResult result) {
return aggregateRepository.findById(new Id(aggregateId))
.doOnSuccess(publishDocument)
.doOnSuccess(s -> result.events().forEach(publishChanged));
}
protected <E> boolean isInstance(Object obj, Class<E> type) {
return Optional.ofNullable(type)
.flatMap(t -> Optional.ofNullable(obj).map(t::isInstance)).orElse(false);
}
}

View File

@@ -21,19 +21,19 @@ import java.util.stream.Stream;
import static java.util.stream.Collectors.toList;
public class AggregateRepositoryImpl<A, C extends Command, E extends DomainEvent,S> implements AggregateRepository<A, C, E, S> {
public class AggregateRepositoryImpl<A, C extends Command,S> implements AggregateRepository<A, C, S> {
private final AtomicReference<Map<Id, List<E>>> eventSource = new AtomicReference<>(new HashMap<>());
private final AtomicReference<Map<Id, List<? extends DomainEvent>>> eventSource = new AtomicReference<>(new HashMap<>());
private final Cache<Id, AggregateActor<A, C, E>> cache = CacheBuilder.newBuilder()
private final Cache<Id, AggregateActor<A, C>> cache = CacheBuilder.newBuilder()
.expireAfterAccess(Duration.ofMinutes(10))
.removalListener((RemovalListener<Id, AggregateActor<A, C, E>>) notification -> notification.getValue().onDestroy())
.removalListener((RemovalListener<Id, AggregateActor<A, C>>) notification -> notification.getValue().onDestroy())
.build();
private final CommandHandler<A, C> commandHandler;
private final EventApplier<A, E> eventApplier;
private final EventApplier<A> eventApplier;
private final Function<Id, A> aggregateFactory;
@@ -41,7 +41,7 @@ public class AggregateRepositoryImpl<A, C extends Command, E extends DomainEvent
public AggregateRepositoryImpl(
CommandHandler<A, C> commandHandler,
EventApplier<A, E> eventApplier,
EventApplier<A> eventApplier,
Function<Id, A> aggregateFactory,
Function<A, S> stateFactory
) {
@@ -51,28 +51,28 @@ public class AggregateRepositoryImpl<A, C extends Command, E extends DomainEvent
this.stateFactory = stateFactory;
}
private List<E> persistAll(Id id, List<E> events) {
private List<? extends DomainEvent> persistAll(Id id, List<? extends DomainEvent> events) {
eventSource.updateAndGet(esMap -> {
var eventsToStore = Optional.ofNullable(esMap.get(id))
.map(es -> Stream.concat(es.stream(), events.stream())
.sorted(Comparator.comparing(DomainEvent::createdAt))
.collect(toList()))
.orElse(events);
.orElse((List<DomainEvent>) events);
esMap.put(id, eventsToStore);
return esMap;
});
return events;
}
private Mono<AggregateActor<A, C, E>> getAggregate(Id id) {
private Mono<AggregateActor<A, C>> getAggregate(Id id) {
return Mono.just(id)
.map(this::getFromCache);
}
private AggregateActor<A, C, E> getFromCache(Id id) {
private AggregateActor<A, C> getFromCache(Id id) {
try {
return cache.get(id, () ->
new AggregateActorImpl<>(
new AggregateActorImpl<A, C>(
id,
commandHandler,
eventApplier,
@@ -85,7 +85,7 @@ public class AggregateRepositoryImpl<A, C extends Command, E extends DomainEvent
}
@Override
public Mono<CommandResult<E>> execute(C cmd, Id aggregateId) {
public Mono<CommandResult> execute(C cmd, Id aggregateId) {
return getAggregate(aggregateId)
.flatMap(a -> a.execute(cmd));
}

View File

@@ -22,7 +22,7 @@ class AggregateActorTest {
.withValue(10)
.build();
var subject = new AggregateActorImpl<TestAggregate, TestAggregateCommand, TestAggregateEvent>(
var subject = new AggregateActorImpl<TestAggregate, TestAggregateCommand>(
new Id(UUID.randomUUID().toString()),
TestFunctions.FN.commandHandler,
TestFunctions.FN.eventApplier,
@@ -31,7 +31,7 @@ class AggregateActorTest {
TestFunctions.FN.persistAll
);
Mono<CommandResult<TestAggregateEvent>> result = subject.execute(command);
Mono<CommandResult> result = subject.execute(command);
StepVerifier.create(result)
.expectNextMatches(r ->
r.commandId().equals(commandId) && r.statusCode().equals(CommandResult.StatusCode.OK))
@@ -40,7 +40,7 @@ class AggregateActorTest {
@Test
void onDestroy() {
var subject = new AggregateActorImpl<TestAggregate, TestAggregateCommand, TestAggregateEvent>(
var subject = new AggregateActorImpl<TestAggregate, TestAggregateCommand>(
new Id(UUID.randomUUID().toString()),
TestFunctions.FN.commandHandler,
TestFunctions.FN.eventApplier,
@@ -60,7 +60,7 @@ class AggregateActorTest {
@Test
void executeParallel() {
var subject = new AggregateActorImpl<TestAggregate, TestAggregateCommand, TestAggregateEvent>(
var subject = new AggregateActorImpl<TestAggregate, TestAggregateCommand>(
new Id(UUID.randomUUID().toString()),
TestFunctions.FN.commandHandler,
TestFunctions.FN.eventApplier,
@@ -84,7 +84,7 @@ class AggregateActorTest {
@Test
public void testRecoveryState() {
var subject = new AggregateActorImpl<TestAggregate, TestAggregateCommand, TestAggregateEvent>(
var subject = new AggregateActorImpl<TestAggregate, TestAggregateCommand>(
new Id(UUID.randomUUID().toString()),
TestFunctions.FN.commandHandler,
TestFunctions.FN.eventApplier,
@@ -105,7 +105,7 @@ class AggregateActorTest {
@Test
public void test_RecoveryStateAndExecuteCommand() {
var subject = new AggregateActorImpl<TestAggregate, TestAggregateCommand, TestAggregateEvent>(
var subject = new AggregateActorImpl<TestAggregate, TestAggregateCommand>(
new Id(UUID.randomUUID().toString()),
TestFunctions.FN.commandHandler,
TestFunctions.FN.eventApplier,

View File

@@ -18,7 +18,7 @@ class AggregateRepositoryImplTest {
private final Function<TestAggregate, Tuple2<Id, Long>> getState = a -> Tuples.of(a.getId(), a.getValue());
AggregateRepositoryImpl<TestAggregate, TestAggregateCommand, TestAggregateEvent,Tuple2<Id, Long>> subject =
AggregateRepositoryImpl<TestAggregate, TestAggregateCommand,Tuple2<Id, Long>> subject =
new AggregateRepositoryImpl<>(
TestFunctions.FN.commandHandler,
TestFunctions.FN.eventApplier,

View File

@@ -2,6 +2,7 @@ package com.mz.reactor.ddd.reactorddd.persistance.aggregate.impl;
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.event.DomainEvent;
import com.mz.reactor.ddd.common.api.event.EventApplier;
import com.mz.reactor.ddd.common.api.valueobject.Id;
@@ -14,13 +15,13 @@ public enum TestFunctions {
public final CommandHandler<TestAggregate, TestAggregateCommand> commandHandler = new CommandHandler<TestAggregate, TestAggregateCommand>() {
@Override
public CommandResult<TestAggregateEvent> execute(TestAggregate aggregate, TestAggregateCommand command) {
public CommandResult execute(TestAggregate aggregate, TestAggregateCommand command) {
try {
TestAggregateEvent event = aggregate.validate(command);
return CommandResult.builder()
.commandId(command.commandId())
.statusCode(CommandResult.StatusCode.OK)
.addEvents(event)
.events(List.of(event))
.build();
} catch (Exception e) {
return CommandResult.fromError(
@@ -32,11 +33,16 @@ public enum TestFunctions {
}
};
public final EventApplier<TestAggregate, TestAggregateEvent> eventApplier = (aggregate, event) -> aggregate.apply((TestAggregateEvent) event);
public final EventApplier<TestAggregate> eventApplier = new EventApplier<TestAggregate>() {
@Override
public <E extends DomainEvent> TestAggregate apply(TestAggregate aggregate, E event) {
return aggregate.apply((TestAggregateEvent) event);
}
};
public final Function<Id, TestAggregate> aggregateFactory = TestAggregate::new;
public final BiFunction<Id, List<TestAggregateEvent>, List<TestAggregateEvent>> persistAll = (id, events) -> events;
public final BiFunction<Id, List<? extends DomainEvent>, List<? extends DomainEvent>> persistAll = (id, events) -> events;
public Function<String, String> mapTest1(String val1, String val2) {
return v -> val1 + " and " + val2 + " not " + v;

View File

@@ -2,9 +2,11 @@ package com.mz.reactor.ddd.reactorddd.persistance.model;
import com.mz.reactor.ddd.common.api.view.DomainView;
import java.time.Instant;
import java.util.Optional;
public class TestView implements DomainView {
private final String id;
private final String value;
private TestView(Builder builder) {
@@ -25,6 +27,11 @@ public class TestView implements DomainView {
return id;
}
@Override
public Optional<String> correlationId() {
return Optional.empty();
}
public static Builder newBuilder() {
return new Builder();
}

View File

@@ -4,10 +4,14 @@ include 'bank-account:bank-account-application'
include 'bank-account:account-domain'
include 'bank-account:transaction-domain'
include 'shared-dependencies'
include 'shared-spring-dependecies'
include 'shared-spring-dependencies'
include 'common-persistence'
include 'common-components'
include 'bank-account:account-domain-api'
include 'bank-account:transaction-domain-api'
include 'bank-account:account-api'
include 'bank-account:account-impl'
include 'bank-account:account-impl'
include 'bank-account:transaction-impl'
include 'bank-account:transaction-api'
include 'bank-account:account-adapters'
include 'bank-account:transaction-adapters'