Update readme and code cleaning
This commit is contained in:
@@ -16,7 +16,7 @@ This demo is implemented using Spring reactor project.
|
||||
* `Account`
|
||||
* `Transaction`
|
||||
|
||||
Those two aggregates representing one bounded context `Bank account`.
|
||||
Those two aggregate roots representing two bounded contexts `Account bounded context` and `Transaction bounded context`.
|
||||
|
||||
## Supported operation by Account aggregate
|
||||
`account-domain-api` module contains definition of contract provided by `Account` aggregate.
|
||||
@@ -24,7 +24,6 @@ Here is the list of basic operations:
|
||||
* create account
|
||||
* deposit money
|
||||
* withdraw money
|
||||
* transfer money between two accounts
|
||||
|
||||
## Supported operation by Transaction aggregate
|
||||
`transaction-domain-api` module contains definition of contract provided by `Transaction` aggregate
|
||||
@@ -32,6 +31,7 @@ Here is the list of basic operations:
|
||||
* create transaction
|
||||
* cancel/rollback transaction
|
||||
* finish transaction
|
||||
* transfer money between two accounts
|
||||
|
||||
## Persistence
|
||||
Implementation is following CQRS and event sourcing design pattern. Persistence API is provided by
|
||||
|
||||
@@ -7,10 +7,7 @@ import com.mz.reactor.ddd.reactorddd.account.domain.command.DepositMoney;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.command.DepositTransferMoney;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.command.WithdrawMoney;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.command.WithdrawTransferMoney;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.event.MoneyDeposited;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.event.TransferMoneyAccountNotFound;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.event.TransferMoneyDeposited;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.event.TransferMoneyWithdrawn;
|
||||
import com.mz.reactor.ddd.reactorddd.account.domain.event.*;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionCreated;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionDepositRolledBack;
|
||||
import com.mz.reactor.ddd.reactorddd.transaction.domain.event.TransactionWithdrawRolledBack;
|
||||
@@ -32,7 +29,11 @@ public class TransactionChangeStreamAdapter {
|
||||
|
||||
private final AccountQuery accountQuery;
|
||||
|
||||
public TransactionChangeStreamAdapter(ApplicationMessageBus messageBus, AccountApplicationService accountService, AccountQuery accountQuery) {
|
||||
public TransactionChangeStreamAdapter(
|
||||
ApplicationMessageBus messageBus,
|
||||
AccountApplicationService accountService,
|
||||
AccountQuery accountQuery
|
||||
) {
|
||||
this.messageBus = Objects.requireNonNull(messageBus);
|
||||
this.accountService = Objects.requireNonNull(accountService);
|
||||
this.accountQuery = Objects.requireNonNull(accountQuery);
|
||||
@@ -100,7 +101,7 @@ public class TransactionChangeStreamAdapter {
|
||||
.amount(e.amount())
|
||||
.aggregateId(e.toAccountId())
|
||||
.correlationId(e.correlationId())
|
||||
.build(), MoneyDeposited.class))
|
||||
.build(), MoneyWithdrawn.class))
|
||||
.log()
|
||||
.retry()
|
||||
.subscribe();
|
||||
|
||||
@@ -8,7 +8,9 @@ import org.immutables.value.Value;
|
||||
@JsonSerialize(as = ImmutableWithdrawTransferMoneyFailed.class)
|
||||
@JsonDeserialize(as = ImmutableWithdrawTransferMoneyFailed.class)
|
||||
public interface WithdrawTransferMoneyFailed extends TransferMoneyFailed {
|
||||
|
||||
static ImmutableWithdrawTransferMoneyFailed.Builder builder() {
|
||||
return ImmutableWithdrawTransferMoneyFailed.builder();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,13 +11,13 @@ import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class AccountAggregate {
|
||||
private Id aggregateId;
|
||||
private final Id aggregateId;
|
||||
|
||||
private Money amount;
|
||||
|
||||
private Set<Id> openedTransactions = new HashSet<>();
|
||||
|
||||
private Set<Id> finishedTransactions = new HashSet<>();
|
||||
private final Set<Id> finishedTransactions = new HashSet<>();
|
||||
|
||||
public AccountAggregate(String aggregateId) {
|
||||
this.aggregateId = new Id(aggregateId);
|
||||
|
||||
@@ -2,5 +2,6 @@ dependencies {
|
||||
api project(':common-api')
|
||||
api project(':common-components')
|
||||
api project(':bank-account:account:account-domain-api')
|
||||
implementation project(':common-persistence-api')
|
||||
implementation project(':bank-account:account:account-api')
|
||||
}
|
||||
@@ -48,7 +48,7 @@ public class AccountHandler implements HttpHandler {
|
||||
|
||||
public Mono<ServerResponse> getAll(ServerRequest request) {
|
||||
return ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(accountQuery.getAll(), AccountState.class);
|
||||
}
|
||||
|
||||
@@ -78,11 +78,11 @@ 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::depositMoney);
|
||||
.route(POST("").and(accept(MediaType.APPLICATION_JSON)), this::createAccount)
|
||||
.andRoute(GET("/").and(accept(MediaType.APPLICATION_JSON)), this::getAll)
|
||||
.andRoute(GET("/{id}").and(accept(MediaType.APPLICATION_JSON)), this::getById)
|
||||
.andRoute(PUT("/moneys/withdraw").and(accept(MediaType.APPLICATION_JSON)), this::withdrawMoney)
|
||||
.andRoute(PUT("/moneys/deposit").and(accept(MediaType.APPLICATION_JSON)), this::depositMoney);
|
||||
|
||||
return RouterFunctions.route()
|
||||
.nest(path("/accounts"), () -> route)
|
||||
|
||||
@@ -31,6 +31,7 @@ public class AccountApplicationServiceImpl implements AccountApplicationService
|
||||
this.aggregateFacade = aggregateFacade;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <R extends DomainEvent> Mono<R> execute(AccountCommand cmd, Class<R> eventType) {
|
||||
return this.aggregateFacade.executeReturnEvent(cmd, cmd.aggregateId(), eventType)
|
||||
.cast(eventType);
|
||||
|
||||
@@ -11,6 +11,7 @@ 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.query.Query;
|
||||
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;
|
||||
@@ -25,12 +26,26 @@ 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";
|
||||
public static final String ACCOUNT_QUERY_SERVICE = "accountQueryService";
|
||||
|
||||
private final AccountEventHandler accountEventApplier = new AccountEventHandler();
|
||||
private final AccountCommandHandler accountCommandHandler = new AccountCommandHandler();
|
||||
private final Function<Id, AccountAggregate> aggregateFactory = id -> new AccountAggregate(id.getValue());
|
||||
private final Function<AccountAggregate, AccountState> stateFactory = AccountAggregate::getState;
|
||||
|
||||
@Bean(ACCOUNT_QUERY_SERVICE)
|
||||
public Query<AccountState> accountQueryService(
|
||||
@Qualifier(ACCOUNT_VIEW_REPOSITORY) ViewRepository<AccountState> viewRepository,
|
||||
ApplicationMessageBus bus
|
||||
) {
|
||||
return Query.of(
|
||||
viewRepository,
|
||||
() -> bus.messagesStream()
|
||||
.filter(m -> m instanceof AccountState)
|
||||
.cast(AccountState.class)
|
||||
);
|
||||
}
|
||||
|
||||
@Bean(ACCOUNT_AGGREGATE_REPOSITORY)
|
||||
public AggregateRepository<AccountAggregate, AccountCommand, AccountState> getAggregateRepository() {
|
||||
return new AggregateRepositoryImpl<>(accountCommandHandler, accountEventApplier, aggregateFactory, stateFactory);
|
||||
|
||||
@@ -40,7 +40,7 @@ public class BankAccountAppConfiguration {
|
||||
.add(accountHandler.route())
|
||||
.add(transactionHandler.route())
|
||||
.GET("/health", req -> ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(Mono.just("Tick"), String.class))
|
||||
.onError(Throwable.class,
|
||||
(throwable, serverRequest) -> HttpHandlers.onError(throwable, serverRequest, error -> log.error("Error: ", error)))
|
||||
|
||||
@@ -61,21 +61,21 @@ public class BankAccountAppTest {
|
||||
.build())
|
||||
.build();
|
||||
return webTestClient.post().uri("/accounts")
|
||||
.contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(BodyInserters.fromObject(createAccount1))
|
||||
.exchange()
|
||||
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
.expectHeader().contentType(MediaType.APPLICATION_JSON)
|
||||
.expectBody(CreateAccountResponse.class)
|
||||
.returnResult().getResponseBody();
|
||||
}
|
||||
|
||||
private List<AccountState> getAllAccounts() {
|
||||
return webTestClient.get().uri("/accounts")
|
||||
.accept(MediaType.APPLICATION_JSON_UTF8)
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
.expectHeader().contentType(MediaType.APPLICATION_JSON)
|
||||
.expectBody(List.class).returnResult().getResponseBody();
|
||||
}
|
||||
|
||||
@@ -90,39 +90,39 @@ public class BankAccountAppTest {
|
||||
.build();
|
||||
|
||||
return webTestClient.post().uri("/transactions")
|
||||
.contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(BodyInserters.fromObject(createTransactionRequest))
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
.expectHeader().contentType(MediaType.APPLICATION_JSON)
|
||||
.expectBody(CreateTransactionResponse.class).returnResult().getResponseBody();
|
||||
}
|
||||
|
||||
private AccountState getAccount(String id) {
|
||||
return webTestClient.get().uri("/accounts/{id}", id)
|
||||
.accept(MediaType.APPLICATION_JSON_UTF8)
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
// .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
// .expectHeader().contentType(MediaType.APPLICATION_JSON)
|
||||
.expectBody(AccountState.class).returnResult().getResponseBody();
|
||||
}
|
||||
|
||||
private String getAccountString(String id) {
|
||||
return webTestClient.get().uri("/accounts/{id}", id)
|
||||
.accept(MediaType.APPLICATION_JSON_UTF8)
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
// .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
// .expectHeader().contentType(MediaType.APPLICATION_JSON)
|
||||
.expectBody(String.class).returnResult().getResponseBody();
|
||||
}
|
||||
|
||||
private TransactionState getTransaction(String id) {
|
||||
return webTestClient.get().uri("/transactions/{id}", id)
|
||||
.accept(MediaType.APPLICATION_JSON_UTF8)
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
.expectHeader().contentType(MediaType.APPLICATION_JSON)
|
||||
.expectBody(TransactionState.class).returnResult().getResponseBody();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ public class AccountChangeStreamAdapter {
|
||||
}
|
||||
|
||||
private void handleTransferMoneyWithdrawn(ApplicationMessageBus bus) {
|
||||
log.debug("handleTransferMoneyDeposited ->");
|
||||
log.debug("handleTransferMoneyWithdrawn ->");
|
||||
bus.messagesStream()
|
||||
.filter(m -> m instanceof TransferMoneyWithdrawn)
|
||||
.cast(TransferMoneyWithdrawn.class)
|
||||
|
||||
@@ -26,7 +26,11 @@ public class TransactionHandler implements HttpHandler {
|
||||
|
||||
private final ObjectMapper mapper;
|
||||
|
||||
public TransactionHandler(TransactionApplicationService transactionApplicationService, TransactionQuery query, ObjectMapper mapper) {
|
||||
public TransactionHandler(
|
||||
TransactionApplicationService transactionApplicationService,
|
||||
TransactionQuery query,
|
||||
ObjectMapper mapper
|
||||
) {
|
||||
this.service = Objects.requireNonNull(transactionApplicationService);
|
||||
this.query = Objects.requireNonNull(query);
|
||||
this.mapper = mapper;
|
||||
@@ -34,7 +38,7 @@ public class TransactionHandler implements HttpHandler {
|
||||
|
||||
public Mono<ServerResponse> getAll(ServerRequest request) {
|
||||
return ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(query.getAll(), TransactionState.class);
|
||||
}
|
||||
|
||||
@@ -54,9 +58,9 @@ public class TransactionHandler implements HttpHandler {
|
||||
@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);
|
||||
.route(POST("").and(accept(MediaType.APPLICATION_JSON)), this::createTransaction)
|
||||
.andRoute(GET("/").and(accept(MediaType.APPLICATION_JSON)), this::getAll)
|
||||
.andRoute(GET("/{id}").and(accept(MediaType.APPLICATION_JSON)), this::getById);
|
||||
|
||||
return RouterFunctions.route()
|
||||
.nest(path("/transactions"), () -> route)
|
||||
|
||||
@@ -9,7 +9,7 @@ import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Scheduler;
|
||||
|
||||
import static com.mz.reactor.ddd.common.components.http.HttpHandlers.deserializeJsonString;
|
||||
import static org.springframework.web.reactive.function.BodyInserters.fromObject;
|
||||
import static org.springframework.web.reactive.function.BodyInserters.fromValue;
|
||||
|
||||
public interface HttpHandler {
|
||||
|
||||
@@ -29,8 +29,8 @@ public interface HttpHandler {
|
||||
|
||||
default <T> Mono<ServerResponse> mapToResponse(T result) {
|
||||
return ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
.body(fromObject(result));
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(fromValue(result));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,18 +13,11 @@ import javax.annotation.Nonnull;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.springframework.web.reactive.function.BodyInserters.fromObject;
|
||||
import static org.springframework.web.reactive.function.BodyInserters.fromValue;
|
||||
|
||||
public final class HttpHandlers {
|
||||
|
||||
private HttpHandlers() {}
|
||||
// private final ObjectMapper mapper;
|
||||
//
|
||||
// HttpHandlerFunctions() {
|
||||
// mapper = new ObjectMapper();
|
||||
// mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
|
||||
// mapper.registerModule(new Jdk8Module());
|
||||
// }
|
||||
|
||||
public static <T> Function<String, Mono<T>> deserializeJsonString(
|
||||
@Nonnull Class<T> clazz,
|
||||
@@ -43,7 +36,7 @@ public final class HttpHandlers {
|
||||
logger.accept(e);
|
||||
return ErrorMessage.builder().error(e.getMessage()).build();
|
||||
}).flatMap(error -> ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
.body(fromObject(error)));
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(fromValue(error)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ public class AggregateFacadeImpl<A, C extends Command, S> implements AggregateFa
|
||||
public Mono<? extends DomainEvent> executeReturnEvent(C command, String aggregateID, Class<? extends DomainEvent> eventType) {
|
||||
var result = aggregateRepository.execute(command, new Id(aggregateID));
|
||||
return result.flatMap(cr -> processResult(aggregateID, eventType, cr))
|
||||
.doOnError(error -> log.error("execute -> event type: "+eventType, error));
|
||||
.doOnError(error -> log.error("execute -> event type: " + eventType, error));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -72,7 +72,7 @@ public class AggregateRepositoryImpl<A, C extends Command, S> implements Aggrega
|
||||
private AggregateActor<A, C> getFromCache(Id id) {
|
||||
try {
|
||||
return cache.get(id, () ->
|
||||
new AggregateActorImpl<A, C>(
|
||||
new AggregateActorImpl<>(
|
||||
id,
|
||||
commandHandler,
|
||||
eventHandler,
|
||||
@@ -100,7 +100,8 @@ public class AggregateRepositoryImpl<A, C extends Command, S> implements Aggrega
|
||||
@Override
|
||||
public Mono<S> findIfExists(Id id) {
|
||||
return Mono.just(id)
|
||||
.flatMap(i -> Optional.ofNullable(cache.getIfPresent(i))
|
||||
.flatMap(
|
||||
i -> Optional.ofNullable(cache.getIfPresent(i))
|
||||
.map(a -> a.getState(this.stateFactory))
|
||||
.orElseGet(Mono::empty)
|
||||
);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.mz.reactor.ddd.reactorddd.persistance.query;
|
||||
|
||||
import com.mz.reactor.ddd.common.api.view.DomainView;
|
||||
import com.mz.reactor.ddd.reactorddd.persistance.query.impl.QueryImpl;
|
||||
import com.mz.reactor.ddd.reactorddd.persistance.view.impl.ViewRepository;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public interface Query<S extends DomainView> {
|
||||
|
||||
Mono<S> findById(String id);
|
||||
|
||||
Flux<S> getAll();
|
||||
|
||||
static <S extends DomainView> Query<S> of(ViewRepository<S> viewRepository, Supplier<Flux<S>> documentStream) {
|
||||
return new QueryImpl<>(viewRepository, documentStream);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.mz.reactor.ddd.reactorddd.persistance.query.impl;
|
||||
|
||||
import com.mz.reactor.ddd.common.api.view.DomainView;
|
||||
import com.mz.reactor.ddd.reactorddd.persistance.query.Query;
|
||||
import com.mz.reactor.ddd.reactorddd.persistance.view.impl.ViewRepository;
|
||||
import org.springframework.lang.NonNull;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
public class QueryImpl<S extends DomainView> implements Query<S> {
|
||||
|
||||
private final ViewRepository<S> repository;
|
||||
|
||||
private final Predicate<S> getAll = v -> true;
|
||||
|
||||
public QueryImpl(
|
||||
ViewRepository<S> repository,
|
||||
Supplier<Flux<S>> documentStream
|
||||
) {
|
||||
this.repository = requireNonNull(repository, "repository is required");
|
||||
requireNonNull(documentStream, "documentStream is required").get()
|
||||
.subscribe(repository::addView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<S> findById(@NonNull String id) {
|
||||
return repository.findBy(view -> view.id().equals(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<S> getAll() {
|
||||
return repository.findAllBy(getAll);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user