Initial check in of Java/Scala Spring Boot example code

This commit is contained in:
Chris Richardson
2014-12-31 15:41:58 -08:00
parent f762c91244
commit da0aaf7d28
123 changed files with 3612 additions and 1 deletions

View File

@@ -1 +1,30 @@
Here is some example Event Sourcing/CQRS-based code along with an embeddable/JDBC-based test eventstore.
#Event-Sourcing+CQRS example application
This example application is the money transfer application described in my talk [Building and deploying microservices with event sourcing, CQRS and Docker](http://plainoldobjects.com/presentations/building-and-deploying-microservices-with-event-sourcing-cqrs-and-docker/).
This talk describe a way of architecting highly scalable and available applications that is based on microservices, polyglot persistence,
event sourcing (ES) and command query responsibility separation (CQRS).
Applications consist of loosely coupled components that communicate using events.
These components can be deployed either as separate services or, as they are here, packaged as a monolithic application for simplified development and testing.
# About the examples
There are currently the following versions of the example application:
* java-spring - a Java and Spring Boot example
* scala-spring - a Scala and Spring Boot example
Other examples will be added shortly including a Scala/Play example.
For more information, please see the [wiki](../../wiki)
# About the event store
The application use an embedded SQL-based event store.
# Running the tests
To run the tests you need to set the following environment variable:
```
export SPRING_DATA_MONGODB_URI=mongodb://192.168.59.103/yourdb
```

6
gradle-all.sh Executable file
View File

@@ -0,0 +1,6 @@
#! /bin/bash -e
for dir in java-spring scala-spring; do
$dir/gradlew -b $dir/build.gradle $*
done

29
java-spring/build.gradle Normal file
View File

@@ -0,0 +1,29 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:1.1.8.RELEASE")
}
}
allprojects {
group = "net.chrisrichardson.eventstore"
}
task wrapper(type: Wrapper) {
gradleVersion = '2.0'
}
subprojects {
apply plugin: 'java'
apply plugin: 'scala'
sourceCompatibility = 1.7
targetCompatibility = 1.7
repositories {
mavenCentral()
maven { url "https://06c59145-4e83-4f22-93ef-6a7eee7aebaa.repos.chrisrichardson.net.s3.amazonaws.com" }
}
}

View File

@@ -0,0 +1,19 @@
apply plugin: 'java'
repositories {
mavenCentral()
}
dependencies {
compile "net.chrisrichardson.eventstore.client:eventstore-java-client:$eventStoreClientVersion"
compile 'com.fasterxml.jackson.core:jackson-core:2.4.3'
compile 'com.fasterxml.jackson.core:jackson-databind:2.4.3'
compile 'com.fasterxml.jackson.module:jackson-module-scala_2.10:2.4.3'
testCompile group: 'junit', name: 'junit', version:'4.11'
testCompile "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
testCompile "net.chrisrichardson.eventstore.client:eventstore-jdbc:$eventStoreClientVersion"
}

View File

@@ -0,0 +1,7 @@
package net.chrisrichardson.eventstorestore.javaexamples.testutil;
import rx.Observable;
public interface Producer<T> {
public Observable<T> produce();
}

View File

@@ -0,0 +1,95 @@
package net.chrisrichardson.eventstorestore.javaexamples.testutil;
import rx.Observable;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.functions.Func2;
import java.util.concurrent.TimeUnit;
public class TestUtil {
public static <T> T await(Observable<T> o) {
return o.single().timeout(1, TimeUnit.SECONDS).toBlocking().getIterator().next();
}
static class Tuple2<A, B> {
private A first;
private B second;
Tuple2(A first, B second) {
this.first = first;
this.second = second;
}
}
interface Outcome<T> {
}
static class Success<T> implements Outcome<T> {
T value;
public Success(T value) {
this.value = value;
}
}
static class Failure<T> implements Outcome<T> {
Throwable t;
public Failure(Throwable t) {
this.t = t;
}
}
public static <T> void eventually(final Producer<T> producer, final Verifier<T> verifier) {
final int n = 50;
Object possibleException = Observable.timer(0, 100, TimeUnit.MILLISECONDS).flatMap(new Func1<Long, Observable<Outcome<T>>>() {
@Override
public Observable<Outcome<T>> call(Long aLong) {
try {
return producer.produce().map(new Func1<T, Outcome<T>>() {
@Override
public Outcome<T> call(T t) {
return new Success<T>(t);
}
});
} catch (Exception e) {
Outcome<T> value = new Failure<T>(e);
return Observable.just(value);
}
}
}).map(new Func1<Outcome<T>, Throwable>() {
@Override
public Throwable call(Outcome<T> t) {
try {
if (t instanceof Success) {
verifier.verify(((Success<T>) t).value);
return null;
} else
return ((Failure<T>) t).t;
} catch (Throwable e) {
return e;
}
}
}).take(n).zipWith(Observable.range(0, n), new Func2<Throwable, Integer, Tuple2<Throwable, Integer>>() {
@Override
public Tuple2<Throwable, Integer> call(Throwable e, Integer idx) {
return new Tuple2<Throwable, Integer>(e, idx);
}
}).skipWhile(new Func1<Tuple2<Throwable, Integer>, Boolean>() {
@Override
public Boolean call(Tuple2<Throwable, Integer> tuple2) {
return tuple2.first != null && tuple2.second < n - 1;
}
}).first().toBlocking().getIterator().next().first;
if (possibleException != null)
throw new RuntimeException((Throwable)possibleException);
}
}

View File

@@ -0,0 +1,5 @@
package net.chrisrichardson.eventstorestore.javaexamples.testutil;
public interface Verifier<T> {
public void verify(T x);
}

View File

@@ -0,0 +1,22 @@
apply plugin: 'scala'
apply plugin: 'spring-boot'
if (!hasProperty("eventstoreType")) {
ext.eventstoreType="jdbc"
}
dependencies {
compile "org.scala-lang:scala-library:2.10.2"
compile project(":eventstore-examples-java")
compile "org.springframework.boot:spring-boot-starter-web"
compile "org.springframework.boot:spring-boot-starter-actuator"
compile "net.chrisrichardson.eventstore.client:eventstore-jdbc:$eventStoreClientVersion"
testCompile "org.springframework.boot:spring-boot-starter-test"
}

View File

@@ -0,0 +1,29 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransferConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts.QuerySideAccountConfiguration;
import net.chrisrichardson.eventstore.jdbc.config.JdbcEventStoreConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
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.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
@Configuration
@Import({AccountConfiguration.class, MoneyTransferConfiguration.class, JdbcEventStoreConfiguration.class, QuerySideAccountConfiguration.class})
@EnableAutoConfiguration
@ComponentScan
public class BankingWebConfiguration {
@Bean
public HttpMessageConverters customConverters() {
HttpMessageConverter<?> additional = new MappingJackson2HttpMessageConverter();
return new HttpMessageConverters(additional);
}
}

View File

@@ -0,0 +1,37 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.accounts;
import net.chrisrichardson.eventstore.EntityWithIdAndVersion;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.Account;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountService;
import net.chrisrichardson.eventstore.javaexamples.banking.web.util.DeferredUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import rx.functions.Func1;
@RestController
@RequestMapping("/accounts")
public class AccountController {
private AccountService accountService;
@Autowired
public AccountController(AccountService accountService) {
this.accountService = accountService;
}
@RequestMapping(method = RequestMethod.POST)
public DeferredResult<CreateAccountResponse> createAccount(@RequestBody CreateAccountRequest request) {
return DeferredUtils.toDeferredResult(accountService.openAccount(request.getInitialBalance()).map(new Func1<EntityWithIdAndVersion<Account>, CreateAccountResponse>() {
@Override
public CreateAccountResponse call(EntityWithIdAndVersion<Account> entityAndEventInfo) {
return new CreateAccountResponse(entityAndEventInfo.getEntityIdentifier().getId());
}
}));
}
}

View File

@@ -0,0 +1,24 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.accounts;
import java.math.BigDecimal;
public class CreateAccountRequest {
private BigDecimal initialBalance;
public CreateAccountRequest() {
}
public CreateAccountRequest(BigDecimal initialBalance) {
this.initialBalance = initialBalance;
}
public BigDecimal getInitialBalance() {
return initialBalance;
}
public void setInitialBalance(BigDecimal initialBalance) {
this.initialBalance = initialBalance;
}
}

View File

@@ -0,0 +1,22 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.accounts;
public class CreateAccountResponse {
private String accountId;
public CreateAccountResponse() {
}
public CreateAccountResponse(String accountId) {
this.accountId = accountId;
}
public String getAccountId() {
return accountId;
}
public void setAccountId(String accountId) {
this.accountId = accountId;
}
}

View File

@@ -0,0 +1,45 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.transactions;
import net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.accounts.CreateAccountResponse;
import java.math.BigDecimal;
public class CreateMoneyTransferRequest {
private String fromAccountId;
private String toAccountId;
private BigDecimal amount;
public CreateMoneyTransferRequest() {
}
public CreateMoneyTransferRequest(String fromAccountId, String toAccountId, BigDecimal amount) {
this.fromAccountId = fromAccountId;
this.toAccountId = toAccountId;
this.amount = amount;
}
public void setFromAccountId(String fromAccountId) {
this.fromAccountId = fromAccountId;
}
public void setToAccountId(String toAccountId) {
this.toAccountId = toAccountId;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
public String getFromAccountId() {
return fromAccountId;
}
public String getToAccountId() {
return toAccountId;
}
public BigDecimal getAmount() {
return amount;
}
}

View File

@@ -0,0 +1,18 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.transactions;
public class CreateMoneyTransferResponse {
private String moneyTransferId;
public CreateMoneyTransferResponse() {
}
public CreateMoneyTransferResponse(String moneyTransferId) {
this.moneyTransferId = moneyTransferId;
}
public String getMoneyTransferId() {
return moneyTransferId;
}
}

View File

@@ -0,0 +1,41 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.transactions;
import net.chrisrichardson.eventstore.EntityIdentifier;
import net.chrisrichardson.eventstore.EntityWithIdAndVersion;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransfer;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransferService;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.TransferDetails;
import net.chrisrichardson.eventstore.javaexamples.banking.web.util.DeferredUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import rx.functions.Func1;
@RestController
@RequestMapping("/transfers")
public class MoneyTransferController {
private final MoneyTransferService moneyTransferService;
@Autowired
public MoneyTransferController(MoneyTransferService moneyTransferService) {
this.moneyTransferService = moneyTransferService;
}
@RequestMapping(method = RequestMethod.POST)
public DeferredResult<CreateMoneyTransferResponse> createMoneyTransfer(@RequestBody CreateMoneyTransferRequest request) {
TransferDetails transferDetails = new TransferDetails(new EntityIdentifier(request.getFromAccountId()), new EntityIdentifier(request.getToAccountId()), request.getAmount());
return DeferredUtils.toDeferredResult(moneyTransferService.transferMoney(transferDetails).map(new Func1<EntityWithIdAndVersion<MoneyTransfer>, CreateMoneyTransferResponse>() {
@Override
public CreateMoneyTransferResponse call(EntityWithIdAndVersion<MoneyTransfer> entityAndEventInfo) {
return new CreateMoneyTransferResponse(entityAndEventInfo.getEntityIdentifier().getId());
}
}));
}
}

View File

@@ -0,0 +1,11 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web.main;
import net.chrisrichardson.eventstore.javaexamples.banking.web.BankingWebConfiguration;
import org.springframework.boot.SpringApplication;
public class BankingMain {
public static void main(String[] args) {
SpringApplication.run(BankingWebConfiguration.class, args);
}
}

View File

@@ -0,0 +1,43 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web.queryside.accounts;
import net.chrisrichardson.eventstore.EntityIdentifier;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts.AccountInfo;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts.AccountNotFoundException;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts.AccountQueryService;
import net.chrisrichardson.eventstore.javaexamples.banking.web.util.DeferredUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.async.DeferredResult;
import rx.functions.Func1;
import java.math.BigDecimal;
@RestController
public class AccountQueryController {
private AccountQueryService accountInfoQueryService;
@Autowired
public AccountQueryController(AccountQueryService accountInfoQueryService) {
this.accountInfoQueryService = accountInfoQueryService;
}
@RequestMapping(value="/accounts/{accountId}", method = RequestMethod.GET)
public DeferredResult<GetAccountResponse> get(@PathVariable String accountId) {
return DeferredUtils.toDeferredResult(accountInfoQueryService.findByAccountId(new EntityIdentifier(accountId)).map(new Func1<AccountInfo, GetAccountResponse>() {
@Override
public GetAccountResponse call(AccountInfo accountInfo) {
return new GetAccountResponse(accountInfo.getId(), new BigDecimal(accountInfo.getBalance()));
}
}));
}
@ResponseStatus(value= HttpStatus.NOT_FOUND, reason="account not found")
@ExceptionHandler(AccountNotFoundException.class)
public void accountNotFound() {
}
}

View File

@@ -0,0 +1,33 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web.queryside.accounts;
import java.math.BigDecimal;
public class GetAccountResponse {
private String accountId;
private BigDecimal balance;
public GetAccountResponse() {
}
public GetAccountResponse(String accountId, BigDecimal balance) {
this.accountId = accountId;
this.balance = balance;
}
public void setBalance(BigDecimal balance) {
this.balance = balance;
}
public void setAccountId(String accountId) {
this.accountId = accountId;
}
public String getAccountId() {
return accountId;
}
public BigDecimal getBalance() {
return balance;
}
}

View File

@@ -0,0 +1,33 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web.util;
import org.springframework.web.context.request.async.DeferredResult;
import rx.Observable;
import rx.Subscriber;
import java.util.concurrent.atomic.AtomicReference;
public class DeferredUtils {
public static <T> DeferredResult<T> toDeferredResult(Observable<T> o) {
final DeferredResult<T> d = new DeferredResult<T>();
final AtomicReference<T> r = new AtomicReference<T>();
o.single().subscribe(new Subscriber<T>() {
@Override
public void onCompleted() {
d.setResult(r.get());
}
@Override
public void onError(Throwable e) {
d.setErrorResult(e);
}
@Override
public void onNext(T t) {
r.set(t);
}
});
return d;
}
}

View File

@@ -0,0 +1,96 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web;
import net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.transactions.CreateMoneyTransferRequest;
import net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.transactions.CreateMoneyTransferResponse;
import net.chrisrichardson.eventstore.javaexamples.banking.web.queryside.accounts.GetAccountResponse;
import net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.accounts.CreateAccountRequest;
import net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.accounts.CreateAccountResponse;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.web.client.RestTemplate;
import java.math.BigDecimal;
import net.chrisrichardson.eventstorestore.javaexamples.testutil.Producer;
import net.chrisrichardson.eventstorestore.javaexamples.testutil.Verifier;
import rx.Observable;
import static net.chrisrichardson.eventstorestore.javaexamples.testutil.TestUtil.eventually;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = BankingWebTestConfiguration.class)
@WebAppConfiguration
@IntegrationTest({"server.port=0", "management.port=0"})
public class BankingWebIntegrationTest {
@Value("${local.server.port}")
private int port;
private String baseUrl(String path) {
return "http://localhost:" + port + "/" + path;
}
@Autowired
RestTemplate restTemplate;
@Test
public void shouldCreateAccountsAndTransferMoney() {
BigDecimal initialFromAccountBalance = new BigDecimal(500);
BigDecimal initialToAccountBalance = new BigDecimal(100);
BigDecimal amountToTransfer = new BigDecimal(150);
BigDecimal finalFromAccountBalance = initialFromAccountBalance.subtract(amountToTransfer);
BigDecimal finalToAccountBalance = initialToAccountBalance.add(amountToTransfer);
final CreateAccountResponse fromAccount = restTemplate.postForEntity(baseUrl("/accounts"), new CreateAccountRequest(initialFromAccountBalance), CreateAccountResponse.class).getBody();
final String fromAccountId = fromAccount.getAccountId();
CreateAccountResponse toAccount = restTemplate.postForEntity(baseUrl("/accounts"), new CreateAccountRequest(initialToAccountBalance), CreateAccountResponse.class).getBody();
String toAccountId = toAccount.getAccountId();
Assert.assertNotNull(fromAccountId);
Assert.assertNotNull(toAccountId);
assertAccountBalance(fromAccountId, initialFromAccountBalance);
assertAccountBalance(toAccountId, initialToAccountBalance);
final CreateMoneyTransferResponse moneyTransfer = restTemplate.postForEntity(baseUrl("/transfers"),
new CreateMoneyTransferRequest(fromAccountId, toAccountId, amountToTransfer), CreateMoneyTransferResponse.class).getBody();
assertAccountBalance(fromAccountId, finalFromAccountBalance);
assertAccountBalance(toAccountId, finalToAccountBalance);
}
private BigDecimal toCents(BigDecimal dollarAmount) {
return dollarAmount.multiply(new BigDecimal(100));
}
private void assertAccountBalance(final String fromAccountId, final BigDecimal expectedBalanceInDollars) {
final BigDecimal inCents = toCents(expectedBalanceInDollars);
eventually(
new Producer<GetAccountResponse>() {
@Override
public Observable<GetAccountResponse> produce() {
return Observable.just(restTemplate.getForEntity(baseUrl("/accounts/" + fromAccountId), GetAccountResponse.class).getBody());
}
},
new Verifier<GetAccountResponse>() {
@Override
public void verify(GetAccountResponse accountInfo) {
Assert.assertEquals(fromAccountId, accountInfo.getAccountId());
Assert.assertEquals(inCents, accountInfo.getBalance());
}
});
}
}

View File

@@ -0,0 +1,30 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.boot.context.embedded.EmbeddedServletContainer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import javax.annotation.PostConstruct;
import java.util.Arrays;
import java.util.List;
@Configuration
@Import(BankingWebConfiguration.class)
public class BankingWebTestConfiguration {
@Bean
public RestTemplate restTemplate(HttpMessageConverters converters) {
RestTemplate restTemplate = new RestTemplate();
HttpMessageConverter<?> httpMessageConverter = converters.getConverters().get(0);
List<? extends HttpMessageConverter<?>> httpMessageConverters = Arrays.asList(new MappingJackson2HttpMessageConverter());
restTemplate.setMessageConverters((List<HttpMessageConverter<?>>) httpMessageConverters);
return restTemplate;
}
}

View File

@@ -0,0 +1,21 @@
apply plugin: 'java'
repositories {
mavenCentral()
}
dependencies {
compile project(":eventstore-examples-java-testutil")
compile "net.chrisrichardson.eventstore.client:eventstore-java-client:$eventStoreClientVersion"
compile "org.springframework.boot:spring-boot-starter-data-mongodb:$springBootVersion"
compile 'com.fasterxml.jackson.core:jackson-core:2.4.3'
compile 'com.fasterxml.jackson.core:jackson-databind:2.4.3'
compile 'com.fasterxml.jackson.module:jackson-module-scala_2.10:2.4.3'
testCompile group: 'junit', name: 'junit', version:'4.11'
testCompile "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
testCompile "net.chrisrichardson.eventstore.client:eventstore-jdbc:$eventStoreClientVersion"
}

View File

@@ -0,0 +1,49 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
import net.chrisrichardson.eventstore.Event;
import net.chrisrichardson.eventstore.EventUtil;
import net.chrisrichardson.eventstore.ReflectiveMutableCommandProcessingAggregate;
import java.math.BigDecimal;
import java.util.List;
public class Account extends ReflectiveMutableCommandProcessingAggregate<Account, AccountCommand> {
private BigDecimal balance;
public List<Event> process(OpenAccountCommand cmd) {
return EventUtil.events(new AccountOpenedEvent(cmd.getInitialBalance()));
}
public List<Event> process(DebitAccountCommand cmd) {
if (balance.compareTo(cmd.getAmount()) < 0)
return EventUtil.events(new AccountDebitFailedDueToInsufficientFundsEvent(cmd.getTransactionId()));
else
return EventUtil.events(new AccountDebitedEvent(cmd.getAmount(), cmd.getTransactionId()));
}
public List<Event> process(CreditAccountCommand cmd) {
return EventUtil.events(new AccountCreditedEvent(cmd.getAmount(), cmd.getTransactionId()));
}
public void apply(AccountOpenedEvent event) {
balance = event.getInitialBalance();
}
public void apply(AccountDebitedEvent event) {
balance = balance.subtract(event.getAmount());
}
public void apply(AccountDebitFailedDueToInsufficientFundsEvent event) {
}
public void apply(AccountCreditedEvent event) {
balance = balance.add(event.getAmount());
}
public BigDecimal getBalance() {
return balance;
}
}

View File

@@ -0,0 +1,28 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
import net.chrisrichardson.eventstore.Aggregate;
import net.chrisrichardson.eventstore.EntityIdentifier;
import net.chrisrichardson.eventstore.Event;
import java.math.BigDecimal;
public class AccountChangedEvent implements Event {
protected BigDecimal amount;
protected EntityIdentifier transactionId;
public AccountChangedEvent(BigDecimal amount, EntityIdentifier transactionId) {
this.amount = amount;
this.transactionId = transactionId;
}
public AccountChangedEvent() {
}
public EntityIdentifier getTransactionId() {
return transactionId;
}
public BigDecimal getAmount() {
return amount;
}
}

View File

@@ -0,0 +1,6 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
import net.chrisrichardson.eventstore.Command;
interface AccountCommand extends Command {
}

View File

@@ -0,0 +1,37 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
import net.chrisrichardson.eventstore.EventStore;
import net.chrisrichardson.eventstore.repository.AggregateRepository;
import net.chrisrichardson.eventstore.subscriptions.EnableEventHandlers;
import net.chrisrichardson.eventstore.subscriptions.EventHandlerRegistrarFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableEventHandlers
public class AccountConfiguration {
@Autowired
private EventHandlerRegistrarFactory eventHandlerRegistrarFactory;
@Bean
public AccountWorkflow accountWorkflow() {
return new AccountWorkflow();
}
@Bean
public AccountService accountService(AggregateRepository<Account, AccountCommand> accountRepository) {
return new AccountService(accountRepository);
}
@Bean
public AggregateRepository<Account, AccountCommand> accountRepository(EventStore eventStore) {
return new AggregateRepository<Account, AccountCommand>(Account.class, eventStore);
}
}

View File

@@ -0,0 +1,17 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
import net.chrisrichardson.eventstore.Aggregate;
import net.chrisrichardson.eventstore.EntityIdentifier;
import java.math.BigDecimal;
public class AccountCreditedEvent extends AccountChangedEvent {
private AccountCreditedEvent() {
}
public AccountCreditedEvent(BigDecimal amount, EntityIdentifier transactionId) {
super(amount, transactionId);
}
}

View File

@@ -0,0 +1,20 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
import net.chrisrichardson.eventstore.Aggregate;
import net.chrisrichardson.eventstore.EntityIdentifier;
import net.chrisrichardson.eventstore.Event;
public class AccountDebitFailedDueToInsufficientFundsEvent implements Event {
private EntityIdentifier transactionId;
private AccountDebitFailedDueToInsufficientFundsEvent() {
}
public AccountDebitFailedDueToInsufficientFundsEvent(EntityIdentifier transactionId) {
this.transactionId = transactionId;
}
public EntityIdentifier getTransactionId() {
return transactionId;
}
}

View File

@@ -0,0 +1,17 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
import net.chrisrichardson.eventstore.Aggregate;
import net.chrisrichardson.eventstore.EntityIdentifier;
import java.math.BigDecimal;
public class AccountDebitedEvent extends AccountChangedEvent {
private AccountDebitedEvent() {
}
public AccountDebitedEvent(BigDecimal amount, EntityIdentifier transactionId) {
super(amount, transactionId);
}
}

View File

@@ -0,0 +1,22 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
import net.chrisrichardson.eventstore.Event;
import java.math.BigDecimal;
public class AccountOpenedEvent implements Event {
private BigDecimal initialBalance;
private AccountOpenedEvent() {
}
public AccountOpenedEvent(BigDecimal initialBalance) {
this.initialBalance = initialBalance;
}
public BigDecimal getInitialBalance() {
return initialBalance;
}
}

View File

@@ -0,0 +1,21 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
import net.chrisrichardson.eventstore.EntityWithIdAndVersion;
import net.chrisrichardson.eventstore.repository.AggregateRepository;
import java.math.BigDecimal;
public class AccountService {
private final AggregateRepository<Account, AccountCommand> accountRepository;
public AccountService(AggregateRepository<Account, AccountCommand> accountRepository) {
this.accountRepository = accountRepository;
}
public rx.Observable<EntityWithIdAndVersion<Account>> openAccount(BigDecimal initialBalance) {
return accountRepository.save(new OpenAccountCommand(initialBalance));
}
}

View File

@@ -0,0 +1,37 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
import net.chrisrichardson.eventstore.Aggregate;
import net.chrisrichardson.eventstore.EntityIdentifier;
import net.chrisrichardson.eventstore.EntityIdentifier;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.DebitRecordedEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransferCreatedEvent;
import net.chrisrichardson.eventstore.subscriptions.*;
import rx.Observable;
import java.math.BigDecimal;
@EventSubscriber(id="accountEventHandlers")
public class AccountWorkflow implements CompoundEventHandler {
@EventHandlerMethod
public Observable<?> debitAccount(EventHandlerContext<MoneyTransferCreatedEvent> ctx) {
MoneyTransferCreatedEvent event = ctx.getEvent();
BigDecimal amount = event.getDetails().getAmount();
EntityIdentifier transactionId = ctx.getEntityIdentifier();
EntityIdentifier fromAccountId = event.getDetails().getFromAccountId();
return ctx.update(Account.class, fromAccountId, new DebitAccountCommand(amount, transactionId));
}
@EventHandlerMethod
public Observable<?> creditAccount(EventHandlerContext<DebitRecordedEvent> ctx) {
DebitRecordedEvent event = ctx.getEvent();
BigDecimal amount = event.getDetails().getAmount();
EntityIdentifier fromAccountId = event.getDetails().getToAccountId();
EntityIdentifier transactionId = ctx.getEntityIdentifier();
return ctx.update(Account.class, fromAccountId, new CreditAccountCommand(amount, transactionId));
}
}

View File

@@ -0,0 +1,25 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
import net.chrisrichardson.eventstore.Aggregate;
import net.chrisrichardson.eventstore.EntityIdentifier;
import java.math.BigDecimal;
public class CreditAccountCommand implements AccountCommand {
private final BigDecimal amount;
private final EntityIdentifier transactionId;
public CreditAccountCommand(BigDecimal amount, EntityIdentifier transactionId) {
this.amount = amount;
this.transactionId = transactionId;
}
public BigDecimal getAmount() {
return amount;
}
public EntityIdentifier getTransactionId() {
return transactionId;
}
}

View File

@@ -0,0 +1,26 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
import net.chrisrichardson.eventstore.Aggregate;
import net.chrisrichardson.eventstore.EntityIdentifier;
import java.math.BigDecimal;
public class DebitAccountCommand implements AccountCommand {
private final BigDecimal amount;
private final EntityIdentifier transactionId;
public DebitAccountCommand(BigDecimal amount, EntityIdentifier transactionId) {
this.amount = amount;
this.transactionId = transactionId;
}
public BigDecimal getAmount() {
return amount;
}
public EntityIdentifier getTransactionId() {
return transactionId;
}
}

View File

@@ -0,0 +1,17 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
import java.math.BigDecimal;
public class OpenAccountCommand implements AccountCommand {
private BigDecimal initialBalance;
public OpenAccountCommand(BigDecimal initialBalance) {
this.initialBalance = initialBalance;
}
public BigDecimal getInitialBalance() {
return initialBalance;
}
}

View File

@@ -0,0 +1,2 @@
@net.chrisrichardson.eventstore.EventEntity(entity="net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.Account")
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;

View File

@@ -0,0 +1,14 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions;
public class CreateMoneyTransferCommand implements MoneyTransferCommand {
private TransferDetails details;
public TransferDetails getDetails() {
return details;
}
public CreateMoneyTransferCommand(TransferDetails details) {
this.details = details;
}
}

View File

@@ -0,0 +1,18 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions;
import net.chrisrichardson.eventstore.Event;
public class CreditRecordedEvent implements Event {
private TransferDetails details;
private CreditRecordedEvent() {
}
public CreditRecordedEvent(TransferDetails details) {
this.details = details;
}
public TransferDetails getDetails() {
return details;
}
}

View File

@@ -0,0 +1,18 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions;
import net.chrisrichardson.eventstore.Event;
public class DebitRecordedEvent implements Event {
private TransferDetails details;
private DebitRecordedEvent() {
}
public DebitRecordedEvent(TransferDetails details) {
this.details = details;
}
public TransferDetails getDetails() {
return details;
}
}

View File

@@ -0,0 +1,18 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions;
import net.chrisrichardson.eventstore.Event;
public class FailedDebitRecordedEvent implements Event {
private TransferDetails details;
private FailedDebitRecordedEvent() {
}
public FailedDebitRecordedEvent(TransferDetails details) {
this.details = details;
}
public TransferDetails getDetails() {
return details;
}
}

View File

@@ -0,0 +1,51 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions;
import net.chrisrichardson.eventstore.Event;
import net.chrisrichardson.eventstore.EventUtil;
import net.chrisrichardson.eventstore.ReflectiveMutableCommandProcessingAggregate;
import java.util.List;
public class MoneyTransfer extends ReflectiveMutableCommandProcessingAggregate<MoneyTransfer, MoneyTransferCommand> {
private TransferDetails details;
private TransferState state;
public List<Event> process(CreateMoneyTransferCommand cmd) {
return EventUtil.events(new MoneyTransferCreatedEvent(cmd.getDetails()));
}
public List<Event> process(RecordDebitCommand cmd) {
return EventUtil.events(new DebitRecordedEvent(details));
}
public List<Event> process(RecordDebitFailedCommand cmd) {
return EventUtil.events(new FailedDebitRecordedEvent(details));
}
public List<Event> process(RecordCreditCommand cmd) {
return EventUtil.events(new CreditRecordedEvent(details));
}
public void apply(MoneyTransferCreatedEvent event) {
this.details = event.getDetails();
this.state = TransferState.INITIAL;
}
public void apply(DebitRecordedEvent event) {
this.state = TransferState.DEBITED;
}
public void apply(FailedDebitRecordedEvent event) {
this.state = TransferState.FAILED_DUE_TO_INSUFFICIENT_FUNDS;
}
public void apply(CreditRecordedEvent event) {
this.state = TransferState.COMPLETED;
}
public TransferState getState() {
return state;
}
}

View File

@@ -0,0 +1,6 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions;
import net.chrisrichardson.eventstore.Command;
interface MoneyTransferCommand extends Command {
}

View File

@@ -0,0 +1,32 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions;
import net.chrisrichardson.eventstore.EventStore;
import net.chrisrichardson.eventstore.repository.AggregateRepository;
import net.chrisrichardson.eventstore.subscriptions.*;
import net.chrisrichardson.eventstore.subscriptions.config.EventStoreSubscriptionsConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import(EventStoreSubscriptionsConfiguration.class)
@EnableEventHandlers
public class MoneyTransferConfiguration {
@Bean
public MoneyTransferService moneyTransferService(AggregateRepository<MoneyTransfer, MoneyTransferCommand> moneyTransferRepository) {
return new MoneyTransferService(moneyTransferRepository);
}
@Bean
public MoneyTransferWorkflow moneyTransferWorkflow() {
return new MoneyTransferWorkflow();
}
@Bean
public AggregateRepository<MoneyTransfer, MoneyTransferCommand> moneyTransferRepository(EventStore eventStore) {
return new AggregateRepository<MoneyTransfer, MoneyTransferCommand>(MoneyTransfer.class, eventStore);
}
}

View File

@@ -0,0 +1,20 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions;
import net.chrisrichardson.eventstore.Event;
public class MoneyTransferCreatedEvent implements Event {
private TransferDetails details;
private MoneyTransferCreatedEvent() {
}
public MoneyTransferCreatedEvent(TransferDetails details) {
this.details = details;
}
public TransferDetails getDetails() {
return details;
}
}

View File

@@ -0,0 +1,20 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions;
import net.chrisrichardson.eventstore.Aggregate;
import net.chrisrichardson.eventstore.EntityEventStore;
import net.chrisrichardson.eventstore.EntityWithIdAndVersion;
import net.chrisrichardson.eventstore.repository.AggregateRepository;
import rx.Observable;
public class MoneyTransferService {
private final AggregateRepository<MoneyTransfer, MoneyTransferCommand> aggregateRepository;
public MoneyTransferService(AggregateRepository<MoneyTransfer, MoneyTransferCommand> aggregateRepository) {
this.aggregateRepository = aggregateRepository;
}
public Observable<EntityWithIdAndVersion<MoneyTransfer>> transferMoney(TransferDetails transferDetails) {
return aggregateRepository.save(new CreateMoneyTransferCommand(transferDetails));
}
}

View File

@@ -0,0 +1,32 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountCreditedEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountDebitFailedDueToInsufficientFundsEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountDebitedEvent;
import net.chrisrichardson.eventstore.subscriptions.CompoundEventHandler;
import net.chrisrichardson.eventstore.subscriptions.EventHandlerContext;
import net.chrisrichardson.eventstore.subscriptions.EventHandlerMethod;
import net.chrisrichardson.eventstore.subscriptions.EventSubscriber;
import rx.Observable;
@EventSubscriber(id="transferEventHandlers")
public class MoneyTransferWorkflow implements CompoundEventHandler {
@EventHandlerMethod
public Observable<?> recordDebit(EventHandlerContext<AccountDebitedEvent> ctx) {
return ctx.update(MoneyTransfer.class, ctx.getEvent().getTransactionId(), new RecordDebitCommand());
}
@EventHandlerMethod
public Observable<?> recordDebitFailed(EventHandlerContext<AccountDebitFailedDueToInsufficientFundsEvent> ctx) {
return ctx.update(MoneyTransfer.class, ctx.getEvent().getTransactionId(), new RecordDebitFailedCommand());
}
@EventHandlerMethod
public Observable<?> recordCredit(EventHandlerContext<AccountCreditedEvent> ctx) {
return ctx.update(MoneyTransfer.class, ctx.getEvent().getTransactionId(), new RecordCreditCommand());
}
}

View File

@@ -0,0 +1,4 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions;
public class RecordCreditCommand implements MoneyTransferCommand {
}

View File

@@ -0,0 +1,4 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions;
public class RecordDebitCommand implements MoneyTransferCommand {
}

View File

@@ -0,0 +1,4 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions;
public class RecordDebitFailedCommand implements MoneyTransferCommand {
}

View File

@@ -0,0 +1,37 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions;
/*
case class TransferDetails(fromAccountId : EntityIdentifier, toAccountId : EntityIdentifier, amount : BigDecimal)
*/
import net.chrisrichardson.eventstore.EntityIdentifier;
import java.math.BigDecimal;
public class TransferDetails {
private EntityIdentifier fromAccountId;
private EntityIdentifier toAccountId;
private BigDecimal amount;
private TransferDetails() {
}
public TransferDetails(EntityIdentifier fromAccountId, EntityIdentifier toAccountId, BigDecimal amount) {
this.fromAccountId = fromAccountId;
this.toAccountId = toAccountId;
this.amount = amount;
}
public EntityIdentifier getFromAccountId() {
return fromAccountId;
}
public EntityIdentifier getToAccountId() {
return toAccountId;
}
public BigDecimal getAmount() {
return amount;
}
}

View File

@@ -0,0 +1,5 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions;
public enum TransferState {
NEW, INITIAL, DEBITED, COMPLETED, FAILED_DUE_TO_INSUFFICIENT_FUNDS
}

View File

@@ -0,0 +1,2 @@
@net.chrisrichardson.eventstore.EventEntity(entity="net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransfer")
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions;

View File

@@ -0,0 +1,18 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
public class AccountChangeInfo {
private String changeId;
private String transactionId;
private String transactionType;
private long amount;
private long balanceDelta;
public AccountChangeInfo(String changeId, String transactionId, String transactionType, long amount, long balanceDelta) {
this.changeId = changeId;
this.transactionId = transactionId;
this.transactionType = transactionType;
this.amount = amount;
this.balanceDelta = balanceDelta;
}
}

View File

@@ -0,0 +1,47 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
import java.util.List;
/**
* Created by cer on 11/21/14.
*/
public class AccountInfo {
private String id;
private long balance;
private List<AccountChangeInfo> changes;
private List<AccountTransactionInfo> transactions;
private String version;
private AccountInfo() {
}
public AccountInfo(String id, long balance, List<AccountChangeInfo> changes, List<AccountTransactionInfo> transactions, String version) {
this.id = id;
this.balance = balance;
this.changes = changes;
this.transactions = transactions;
this.version = version;
}
public String getId() {
return id;
}
public long getBalance() {
return balance;
}
public List<AccountChangeInfo> getChanges() {
return changes;
}
public List<AccountTransactionInfo> getTransactions() {
return transactions;
}
public String getVersion() {
return version;
}
}

View File

@@ -0,0 +1,6 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
import org.springframework.data.mongodb.repository.MongoRepository;
interface AccountInfoRepository extends MongoRepository<AccountInfo, String> {
}

View File

@@ -0,0 +1,65 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
import com.mongodb.WriteResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import java.math.BigDecimal;
import java.util.Collections;
import static net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts.MoneyUtil.toIntegerRepr;
import static org.springframework.data.mongodb.core.query.Criteria.where;
public class AccountInfoUpdateService {
private Logger logger = LoggerFactory.getLogger(getClass());
private AccountInfoRepository accountInfoRepository;
private MongoTemplate mongoTemplate;
public AccountInfoUpdateService(AccountInfoRepository accountInfoRepository, MongoTemplate mongoTemplate) {
this.accountInfoRepository = accountInfoRepository;
this.mongoTemplate = mongoTemplate;
}
public void create(String accountId, BigDecimal initialBalance, String version) {
try {
accountInfoRepository.save(new AccountInfo(
accountId,
toIntegerRepr(initialBalance),
Collections.<AccountChangeInfo>emptyList(),
Collections.<AccountTransactionInfo>emptyList(),
version));
logger.info("Saved in mongo");
} catch (Throwable t) {
logger.error("Error during saving: ");
logger.error("Error during saving: ", t);
throw new RuntimeException(t);
}
}
public void addTransaction(String eventId, String fromAccountId, AccountTransactionInfo ti) {
mongoTemplate.updateMulti(new Query(where("id").is(fromAccountId).and("version").lt(eventId)),
new Update().
push("transactions", ti).
set("version", eventId),
AccountInfo.class);
}
public void updateBalance(String accountId, String changeId, long balanceDelta, AccountChangeInfo ci) {
WriteResult x = mongoTemplate.updateMulti(new Query(where("id").is(accountId).and("version").lt(changeId)),
new Update().
inc("balance", balanceDelta).
push("changes", ci).
set("version", changeId),
AccountInfo.class);
}
}

View File

@@ -0,0 +1,8 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
public class AccountNotFoundException extends RuntimeException {
public AccountNotFoundException(String accountId) {
super("Account not found " + accountId);
}
}

View File

@@ -0,0 +1,23 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
import net.chrisrichardson.eventstore.Aggregate;
import net.chrisrichardson.eventstore.EntityIdentifier;
import net.chrisrichardson.eventstore.EntityNotFoundException;
import rx.Observable;
public class AccountQueryService {
private AccountInfoRepository accountInfoRepository;
public AccountQueryService(AccountInfoRepository accountInfoRepository) {
this.accountInfoRepository = accountInfoRepository;
}
public Observable<AccountInfo> findByAccountId(EntityIdentifier accountId) {
AccountInfo account = accountInfoRepository.findOne(accountId.getId());
if (account == null)
return Observable.error(new AccountNotFoundException(accountId.getId()));
else
return Observable.just(account);
}
}

View File

@@ -0,0 +1,85 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
import net.chrisrichardson.eventstore.EntityIdentifier;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountChangedEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountCreditedEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountDebitedEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountOpenedEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransferCreatedEvent;
import net.chrisrichardson.eventstore.subscriptions.CompoundEventHandler;
import net.chrisrichardson.eventstore.subscriptions.DispatchedEvent;
import net.chrisrichardson.eventstore.subscriptions.EventHandlerMethod;
import net.chrisrichardson.eventstore.subscriptions.EventSubscriber;
import rx.Observable;
import java.math.BigDecimal;
import static net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts.MoneyUtil.toIntegerRepr;
@EventSubscriber(id="querySideEventHandlers")
public class AccountQueryWorkflow implements CompoundEventHandler {
private AccountInfoUpdateService accountInfoUpdateService;
public AccountQueryWorkflow(AccountInfoUpdateService accountInfoUpdateService) {
this.accountInfoUpdateService = accountInfoUpdateService;
}
@EventHandlerMethod
public Observable<Object> create(DispatchedEvent<AccountOpenedEvent> de) {
AccountOpenedEvent event = de.event();
String id = de.getEntityIdentifier().getId();
String eventId = de.eventId().asString();
System.out.println("**************** account version=" + id + ", " + eventId);
BigDecimal initialBalance = event.getInitialBalance();
accountInfoUpdateService.create(id, initialBalance, eventId);
return Observable.just(null);
}
@EventHandlerMethod
public Observable<Object> recordTransfer(DispatchedEvent<MoneyTransferCreatedEvent> de) {
String eventId = de.eventId().asString();
String moneyTransferId = de.getEntityIdentifier().getId();
String fromAccountId = de.event().getDetails().getFromAccountId().getId();
String toAccountId = de.event().getDetails().getToAccountId().getId();
System.out.println("**************** account version=" + fromAccountId + ", " + de.eventId().asString());
System.out.println("**************** account version=" + toAccountId + ", " + de.eventId().asString());
AccountTransactionInfo ti = new AccountTransactionInfo(moneyTransferId, fromAccountId, toAccountId, toIntegerRepr(de.event().getDetails().getAmount()));
accountInfoUpdateService.addTransaction(eventId, fromAccountId, ti);
accountInfoUpdateService.addTransaction(eventId, toAccountId, ti);
return Observable.just(null);
}
@EventHandlerMethod
public Observable<Object> recordDebit(DispatchedEvent<AccountDebitedEvent> de) {
return saveChange(de, -1);
}
@EventHandlerMethod
public Observable<Object> recordCredit(DispatchedEvent<AccountCreditedEvent> de) {
return saveChange(de, +1);
}
public <T extends AccountChangedEvent> Observable<Object> saveChange(DispatchedEvent<T> de, int delta) {
String changeId = de.eventId().asString();
String transactionId = de.event().getTransactionId().getId();
long amount = toIntegerRepr(de.event().getAmount());
long balanceDelta = amount * delta;
AccountChangeInfo ci = new AccountChangeInfo(changeId, transactionId, de.event().getClass().getSimpleName(), amount, balanceDelta);
String accountId = de.getEntityIdentifier().getId();
System.out.println("**************** account version=" + accountId + ", " + de.eventId().asString());
accountInfoUpdateService.updateBalance(accountId, changeId, balanceDelta, ci);
return Observable.just(null);
}
}

View File

@@ -0,0 +1,18 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
public class AccountTransactionInfo {
// case class AccountTransactionInfo(transactionId : String, fromAccountId: String, toAccountId: String, amount : Long)
private String transactionId;
private String fromAccountId;
private String toAccountId;
private long amount;
public AccountTransactionInfo(String transactionId, String fromAccountId, String toAccountId, long amount) {
this.transactionId = transactionId;
this.fromAccountId = fromAccountId;
this.toAccountId = toAccountId;
this.amount = amount;
}
}

View File

@@ -0,0 +1,11 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
import java.math.BigDecimal;
public class MoneyUtil {
public static long toIntegerRepr(BigDecimal d) {
return d.multiply(new BigDecimal(100)).longValueExact();
}
}

View File

@@ -0,0 +1,39 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
import net.chrisrichardson.eventstore.subscriptions.EnableEventHandlers;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
@Configuration
@EnableMongoRepositories
@EnableEventHandlers
public class QuerySideAccountConfiguration {
@Bean
public AccountQueryWorkflow accountQueryWorkflow(AccountInfoUpdateService accountInfoUpdateService) {
return new AccountQueryWorkflow(accountInfoUpdateService);
}
@Bean
public AccountInfoUpdateService accountInfoUpdateService(AccountInfoRepository accountInfoRepository, MongoTemplate mongoTemplate) {
return new AccountInfoUpdateService(accountInfoRepository, mongoTemplate);
}
@Bean
public AccountQueryService accountInfoQueryService(AccountInfoRepository accountInfoRepository) {
return new AccountQueryService(accountInfoRepository);
}
@Bean
public QuerySideDependencyChecker querysideDependencyChecker(MongoTemplate mongoTemplate) {
return new QuerySideDependencyChecker(mongoTemplate);
}
}

View File

@@ -0,0 +1,41 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import rx.Observable;
import rx.Subscriber;
import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;
public class QuerySideDependencyChecker {
private Logger logger = LoggerFactory.getLogger(getClass());
private MongoTemplate mongoTemplate;
public QuerySideDependencyChecker(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
@PostConstruct
public void checkDependencies() {
try {
logger.info("Checking mongodb connectivity {}", System.getenv("SPRING_DATA_MONGODB_URI"));
Observable.<Object>create(new Observable.OnSubscribe<Object>() {
@Override
public void call(Subscriber<? super Object> subscriber) {
try {
subscriber.onNext(mongoTemplate.getDb().getCollectionNames());
subscriber.onCompleted();
} catch (Throwable t) {
subscriber.onError(t);
}
}
}).timeout(5, TimeUnit.SECONDS).toBlocking().first();
} catch (Throwable e) {
throw new RuntimeException("Error connecting to Mongo - have you set SPRING_DATA_MONGODB_URI or --spring.data.mongodb_uri?", e);
}
}
}

View File

@@ -0,0 +1,14 @@
package net.chrisrichardson.eventstore.javaexamples.banking;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransferConfiguration;
import net.chrisrichardson.eventstore.jdbc.config.JdbcEventStoreConfiguration;
import net.chrisrichardson.utils.config.MetricRegistryConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import({AccountConfiguration.class, MoneyTransferConfiguration.class, JdbcEventStoreConfiguration.class})
public class BankingTestConfiguration {
}

View File

@@ -0,0 +1,157 @@
package net.chrisrichardson.eventstore.javaexamples.banking;
import net.chrisrichardson.eventstore.EntityEventStore;
import net.chrisrichardson.eventstore.EntityWithIdAndVersion;
import net.chrisrichardson.eventstore.EntityWithMetadata;
import net.chrisrichardson.eventstore.EventStore;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.Account;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountService;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransfer;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransferService;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.TransferDetails;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.TransferState;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import rx.Observable;
import java.math.BigDecimal;
import net.chrisrichardson.eventstorestore.javaexamples.testutil.Producer;
import net.chrisrichardson.eventstorestore.javaexamples.testutil.Verifier;
import static net.chrisrichardson.eventstorestore.javaexamples.testutil.TestUtil.await;
import static net.chrisrichardson.eventstorestore.javaexamples.testutil.TestUtil.eventually;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes=BankingTestConfiguration.class)
@IntegrationTest
public class MoneyTransferIntegrationTest {
@Autowired
private AccountService accountService;
@Autowired
private MoneyTransferService moneyTransferService;
@Autowired
private EventStore eventStore;
@Test
public void shouldTransferMoney() {
final EntityWithIdAndVersion<Account> fromAccount= await(accountService.openAccount(new BigDecimal(150)));
final EntityWithIdAndVersion<Account> toAccount = await(accountService.openAccount(new BigDecimal(300)));
final EntityWithIdAndVersion<MoneyTransfer> transaction = await(
moneyTransferService.transferMoney(new TransferDetails(fromAccount.getEntityIdentifier(),
toAccount.getEntityIdentifier(),
new BigDecimal(80))));
eventually (
new Producer<EntityWithMetadata<Account>>() {
@Override
public Observable<EntityWithMetadata<Account>> produce() {
return (Observable<EntityWithMetadata<Account>>)eventStore.find(Account.class, fromAccount.getEntityIdentifier());
}
},
new Verifier<EntityWithMetadata<Account>>() {
@Override
public void verify(EntityWithMetadata<Account> account) {
Assert.assertEquals(new BigDecimal(70), account.entity().getBalance());
}
});
eventually (
new Producer<EntityWithMetadata<Account>>() {
@Override
public Observable<EntityWithMetadata<Account>> produce() {
return (Observable<EntityWithMetadata<Account>>)eventStore.find(Account.class, toAccount.getEntityIdentifier());
}
},
new Verifier<EntityWithMetadata<Account>>() {
@Override
public void verify(EntityWithMetadata<Account> account) {
Assert.assertEquals(new BigDecimal(380), account.entity().getBalance());
}
});
eventually (
new Producer<EntityWithMetadata<MoneyTransfer>>() {
@Override
public Observable<EntityWithMetadata<MoneyTransfer>> produce() {
return (Observable<EntityWithMetadata<MoneyTransfer>>)eventStore.find(MoneyTransfer.class, transaction.getEntityIdentifier());
}
},
new Verifier<EntityWithMetadata<MoneyTransfer>>() {
@Override
public void verify(EntityWithMetadata<MoneyTransfer> updatedTransaction) {
Assert.assertEquals(TransferState.COMPLETED, updatedTransaction.entity().getState());
}
});
}
@Test
public void shouldFailDueToInsufficientFunds() {
final EntityWithIdAndVersion<Account> fromAccount= await(accountService.openAccount(new BigDecimal(150)));
final EntityWithIdAndVersion<Account> toAccount = await(accountService.openAccount(new BigDecimal(300)));
final EntityWithIdAndVersion<MoneyTransfer> transaction = await(
moneyTransferService.transferMoney(new TransferDetails(fromAccount.getEntityIdentifier(),
toAccount.getEntityIdentifier(),
new BigDecimal(200))));
eventually (
new Producer<EntityWithMetadata<MoneyTransfer>>() {
@Override
public Observable<EntityWithMetadata<MoneyTransfer>> produce() {
return (Observable<EntityWithMetadata<MoneyTransfer>>)eventStore.find(MoneyTransfer.class, transaction.getEntityIdentifier());
}
},
new Verifier<EntityWithMetadata<MoneyTransfer>>() {
@Override
public void verify(EntityWithMetadata<MoneyTransfer> updatedTransaction) {
Assert.assertEquals(TransferState.FAILED_DUE_TO_INSUFFICIENT_FUNDS, updatedTransaction.entity().getState());
}
});
eventually (
new Producer<EntityWithMetadata<Account>>() {
@Override
public Observable<EntityWithMetadata<Account>> produce() {
return (Observable<EntityWithMetadata<Account>>)eventStore.find(Account.class, fromAccount.getEntityIdentifier());
}
},
new Verifier<EntityWithMetadata<Account>>() {
@Override
public void verify(EntityWithMetadata<Account> account) {
Assert.assertEquals(new BigDecimal(150), account.entity().getBalance());
}
});
eventually (
new Producer<EntityWithMetadata<Account>>() {
@Override
public Observable<EntityWithMetadata<Account>> produce() {
return (Observable<EntityWithMetadata<Account>>)eventStore.find(Account.class, toAccount.getEntityIdentifier());
}
},
new Verifier<EntityWithMetadata<Account>>() {
@Override
public void verify(EntityWithMetadata<Account> account) {
Assert.assertEquals(new BigDecimal(300), account.entity().getBalance());
}
});
}
}

View File

@@ -0,0 +1,39 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside;
import net.chrisrichardson.eventstore.Aggregate;
import net.chrisrichardson.eventstore.ReflectiveMutableCommandProcessingAggregate;
import net.chrisrichardson.eventstore.subscriptions.EventEntityUtil;
import org.springframework.util.ReflectionUtils;
import org.junit.Test;
import java.lang.reflect.Method;
public abstract class AbstractEntityEventTest {
@Test
public void eventDefinitionsShouldBeCorrect() {
ReflectionUtils.doWithMethods(entityClass(), new ReflectionUtils.MethodCallback() {
@Override
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
Class eventClass = method.getParameterTypes()[0];
String entityClassName = EventEntityUtil.entityClassFor(eventClass);
try {
Class.forName(entityClassName);
} catch (ClassNotFoundException e) {
throw new RuntimeException("for " + entityClassName, e);
}
}
},
new ReflectionUtils.MethodFilter() {
@Override
public boolean matches(Method method) {
return method.getName().startsWith("apply") && method.getDeclaringClass() != Aggregate.class && method.getDeclaringClass() != ReflectiveMutableCommandProcessingAggregate.class;
}
});
}
protected abstract Class<?> entityClass();
}

View File

@@ -0,0 +1,12 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.AbstractEntityEventTest;
public class AccountEventTest extends AbstractEntityEventTest {
@Override
protected Class<Account> entityClass() {
return Account.class;
}
}

View File

@@ -0,0 +1,23 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
import net.chrisrichardson.eventstore.json.EventStoreCommonObjectMapping;
import net.chrisrichardson.utils.json.JSonMapper;
import org.junit.Assert;
import org.junit.Test;
import java.math.BigDecimal;
public class AccountOpenEventSerializationTest {
@Test
public void shouldSerde() {
AccountOpenedEvent event = new AccountOpenedEvent(new BigDecimal(55));
String json = JSonMapper.toJson(event, EventStoreCommonObjectMapping.getObjectMapper());
System.out.println("json=" + json);
AccountOpenedEvent event2 = JSonMapper.fromJSon(AccountOpenedEvent.class, json, EventStoreCommonObjectMapping.getObjectMapper());
Assert.assertEquals(event.getInitialBalance(), event2.getInitialBalance());
}
}

View File

@@ -0,0 +1,26 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
import net.chrisrichardson.eventstore.CommandProcessingAggregates;
import net.chrisrichardson.eventstore.Event;
import org.junit.Assert;
import org.junit.Test;
import java.math.BigDecimal;
import java.util.List;
public class AccountTest {
@Test
public void testSomething() {
Account account = new Account();
BigDecimal initialBalance = new BigDecimal(512);
List<Event> events = CommandProcessingAggregates.processToList(account, (AccountCommand)new OpenAccountCommand(initialBalance));
Assert.assertEquals(1, events.size());
Assert.assertEquals(AccountOpenedEvent.class, events.get(0).getClass());
account.applyEvent(events.get(0));
Assert.assertEquals(initialBalance, account.getBalance());
}
}

View File

@@ -0,0 +1,12 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.AbstractEntityEventTest;
public class MoneyTransferEventTest extends AbstractEntityEventTest {
@Override
protected Class<?> entityClass() {
return MoneyTransfer.class;
}
}

View File

@@ -0,0 +1,99 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
import net.chrisrichardson.eventstore.EntityEventStore;
import net.chrisrichardson.eventstore.EntityWithIdAndVersion;
import net.chrisrichardson.eventstore.EntityWithMetadata;
import net.chrisrichardson.eventstore.EventStore;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.Account;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountService;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransfer;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransferService;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.TransferDetails;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.TransferState;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import rx.Observable;
import java.math.BigDecimal;
import net.chrisrichardson.eventstorestore.javaexamples.testutil.Producer;
import net.chrisrichardson.eventstorestore.javaexamples.testutil.Verifier;
import static net.chrisrichardson.eventstorestore.javaexamples.testutil.TestUtil.await;
import static net.chrisrichardson.eventstorestore.javaexamples.testutil.TestUtil.eventually;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = AccountQuerySideTestConfiguration.class)
@IntegrationTest
public class AccountQuerySideIntegrationTest {
@Autowired
private AccountService accountService;
@Autowired
private MoneyTransferService moneyTransferService;
@Autowired
private EventStore eventStore;
@Autowired
private AccountQueryService accountQueryService;
@Test
public void shouldUpdateQuerySide() throws Exception {
final EntityWithIdAndVersion<Account> fromAccount = await(accountService.openAccount(new BigDecimal(150)));
final EntityWithIdAndVersion<Account> toAccount = await(accountService.openAccount(new BigDecimal(300)));
final EntityWithIdAndVersion<MoneyTransfer> transaction = await(
moneyTransferService.transferMoney(new TransferDetails(fromAccount.getEntityIdentifier(),
toAccount.getEntityIdentifier(),
new BigDecimal(80))));
eventually(
new Producer<EntityWithMetadata<MoneyTransfer>>() {
@Override
public Observable<EntityWithMetadata<MoneyTransfer>> produce() {
return eventStore.find(MoneyTransfer.class, transaction.getEntityIdentifier());
}
},
new Verifier<EntityWithMetadata<MoneyTransfer>>() {
@Override
public void verify(EntityWithMetadata<MoneyTransfer> updatedTransaction) {
Assert.assertEquals(TransferState.COMPLETED, updatedTransaction.entity().getState());
}
});
eventually(
new Producer<AccountInfo>() {
@Override
public Observable<AccountInfo> produce() {
return accountQueryService.findByAccountId(fromAccount.getEntityIdentifier());
}
},
new Verifier<AccountInfo>() {
@Override
public void verify(AccountInfo accountInfo) {
Assert.assertEquals(70*100, accountInfo.getBalance());
}
});
eventually(
new Producer<AccountInfo>() {
@Override
public Observable<AccountInfo> produce() {
return accountQueryService.findByAccountId(toAccount.getEntityIdentifier());
}
},
new Verifier<AccountInfo>() {
@Override
public void verify(AccountInfo accountInfo) {
Assert.assertEquals(380*100, accountInfo.getBalance());
}
});
}
}

View File

@@ -0,0 +1,10 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
import net.chrisrichardson.eventstore.javaexamples.banking.BankingTestConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import({BankingTestConfiguration.class, QuerySideAccountConfiguration.class})
public class AccountQuerySideTestConfiguration {
}

View File

@@ -0,0 +1,17 @@
org.gradle.jvmargs=-XX:MaxPermSize=512m
# Was 3.2.4.RELEASE
springFrameworkVersion=4.0.3.RELEASE
#rxJavaVersion=0.13.2
rxJavaVersion=0.20.4
scalaTestDependency=org.scalatest:scalatest_2.10:2.0
akkaDependency=com.typesafe.akka:akka-actor_2.10:2.3.4
akkaTestDependency=com.typesafe.akka:akka-actor-tests_2.10:2.3.4
akkaSlf4jDependency=com.typesafe.akka:akka-slf4j_2.10:2.3.4
springBootVersion=1.1.10.RELEASE
eventStoreCommonVersion=0.2
eventStoreClientVersion=0.2

Binary file not shown.

View File

@@ -0,0 +1,6 @@
#Wed Oct 29 07:09:05 PDT 2014
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=http\://services.gradle.org/distributions/gradle-2.0-bin.zip

164
java-spring/gradlew vendored Executable file
View File

@@ -0,0 +1,164 @@
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# For Cygwin, ensure paths are in UNIX format before anything is touched.
if $cygwin ; then
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
fi
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >&-
APP_HOME="`pwd -P`"
cd "$SAVED" >&-
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

90
java-spring/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,90 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1,5 @@
include 'eventstore-examples'
include 'eventstore-examples-main'
include 'eventstore-examples-java'
include 'eventstore-examples-java-web'
include 'eventstore-examples-java-testutil'

29
scala-spring/build.gradle Normal file
View File

@@ -0,0 +1,29 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:1.1.8.RELEASE")
}
}
allprojects {
group = "net.chrisrichardson.eventstore"
}
task wrapper(type: Wrapper) {
gradleVersion = '2.0'
}
subprojects {
apply plugin: 'java'
apply plugin: 'scala'
sourceCompatibility = 1.7
targetCompatibility = 1.7
repositories {
mavenCentral()
maven { url "https://06c59145-4e83-4f22-93ef-6a7eee7aebaa.repos.chrisrichardson.net.s3.amazonaws.com" }
}
}

View File

@@ -0,0 +1,23 @@
apply plugin: 'scala'
apply plugin: 'spring-boot'
if (!hasProperty("eventstoreType")) {
ext.eventstoreType="jdbc"
}
dependencies {
compile "org.scala-lang:scala-library:2.10.2"
compile project(":eventstore-examples")
compile "org.springframework.boot:spring-boot-starter-web"
compile "org.springframework.boot:spring-boot-starter-actuator"
compile "net.chrisrichardson.eventstore.client:eventstore-jdbc:$eventStoreClientVersion"
testCompile "org.springframework.boot:spring-boot-starter-test"
testCompile scalaTestDependency
}

View File

@@ -0,0 +1,16 @@
package net.chrisrichardson.eventstore.examples.bank.web
import net.chrisrichardson.eventstore.embedded.config.EmbeddedEventStoreConfiguration
import net.chrisrichardson.eventstore.examples.bank.accounts.AccountConfiguration
import net.chrisrichardson.eventstore.examples.bank.queryside.QuerySideConfiguration
import net.chrisrichardson.eventstore.examples.bank.transactions.TransactionConfiguration
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.context.annotation._
@Configuration
@EnableAutoConfiguration
@Import(Array(classOf[AccountConfiguration], classOf[TransactionConfiguration], classOf[QuerySideConfiguration], classOf[EmbeddedEventStoreConfiguration]))
@ComponentScan
class BankingWebAppConfiguration {
}

View File

@@ -0,0 +1,20 @@
package net.chrisrichardson.eventstore.examples.bank.web.accounts.controllers
import net.chrisrichardson.eventstore.EntityId
import net.chrisrichardson.eventstore.EventStore
import net.chrisrichardson.eventstore.examples.bank.accounts.{Account, AccountService}
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation._
import scala.concurrent.ExecutionContext.Implicits.global
@RestController
class AccountController @Autowired() (accountService : AccountService, eventStore : EventStore) {
@RequestMapping(value=Array("/accounts"), method = Array(RequestMethod.POST))
def create(@RequestBody request : CreateAccountRequest) = {
val f = accountService.openAccount(request.initialBalance)
WebUtil.toDeferredResult(f map(account => CreateAccountResponse(account.entityId.id)))
}
}

View File

@@ -0,0 +1,6 @@
package net.chrisrichardson.eventstore.examples.bank.web.accounts.controllers
/**
* Created by cer on 7/16/14.
*/
case class CreateAccountRequest(initialBalance : BigDecimal)

View File

@@ -0,0 +1,7 @@
package net.chrisrichardson.eventstore.examples.bank.web.accounts.controllers
/**
* Created by cer on 7/16/14.
*/
case class CreateAccountResponse(accountId : String)
case class GetAccountResponse(accountId : String, balance : String)

View File

@@ -0,0 +1,23 @@
package net.chrisrichardson.eventstore.examples.bank.web.accounts.controllers
import org.springframework.web.context.request.async.DeferredResult
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
/**
* Created by cer on 7/16/14.
*/
object WebUtil {
def toDeferredResult[T](future: Future[T]): DeferredResult[T] = {
val result = new DeferredResult[T]
future onSuccess {
case r => result.setResult(r)
}
future onFailure {
case t => result.setErrorResult(t)
}
result
}
}

View File

@@ -0,0 +1,10 @@
package net.chrisrichardson.eventstore.examples.bank.web.main
import net.chrisrichardson.eventstore.examples.bank.web.BankingWebAppConfiguration
import org.springframework.boot.SpringApplication
object BankingMain {
def main(args: Array[String]) : Unit = SpringApplication.run(classOf[BankingWebAppConfiguration], args :_ *)
}

View File

@@ -0,0 +1,17 @@
package net.chrisrichardson.eventstore.examples.bank.web.queryside.controllers
import net.chrisrichardson.eventstore.EntityId
import net.chrisrichardson.eventstore.EventStore
import net.chrisrichardson.eventstore.examples.bank.accounts.{Account, AccountService}
import net.chrisrichardson.eventstore.examples.bank.queryside.AccountInfoQueryService
import net.chrisrichardson.eventstore.examples.bank.web.accounts.controllers.{GetAccountResponse, WebUtil}
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.{PathVariable, RequestMethod, RequestMapping, RestController}
@RestController
class AccountQuerySideController @Autowired() (accountInfoQueryService : AccountInfoQueryService) {
@RequestMapping(value=Array("/accounts/{accountId}"), method = Array(RequestMethod.GET))
def get(@PathVariable accountId : String) = accountInfoQueryService.findByAccountId(EntityId(accountId))
}

View File

@@ -0,0 +1,11 @@
package net.chrisrichardson.eventstore.examples.bank.web.transactions.controllers
import net.chrisrichardson.eventstore.EntityId
import net.chrisrichardson.eventstore.examples.bank.transactions.TransferStates
case class CreateMoneyTransferResponse(transactionId : String)
case class MoneyTransferRequest(fromAccountId : EntityId, toAccountId : EntityId, amount: BigDecimal)
case class GetMoneyTransferResponse(transactionId : String, status : String)

View File

@@ -0,0 +1,30 @@
package net.chrisrichardson.eventstore.examples.bank.web.transactions.controllers
import net.chrisrichardson.eventstore.EntityId
import net.chrisrichardson.eventstore.EventStore
import net.chrisrichardson.eventstore.examples.bank.accounts.Account
import net.chrisrichardson.eventstore.examples.bank.transactions.{TransferDetails, MoneyTransfer, MoneyTransferService}
import net.chrisrichardson.eventstore.examples.bank.web.accounts.controllers.{GetAccountResponse, WebUtil}
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation._
import scala.concurrent.ExecutionContext.Implicits.global
@RestController
class MoneyTransferController @Autowired()(moneyTransferService : MoneyTransferService,
eventStore : EventStore) {
@RequestMapping(value=Array("/transfers"), method = Array(RequestMethod.POST))
def create(@RequestBody transferDetails : TransferDetails) = WebUtil.toDeferredResult {
for (transaction <- moneyTransferService.transferMoney(transferDetails))
yield CreateMoneyTransferResponse(transaction.entityId.id)
}
@RequestMapping(value=Array("/transfers/{transferId}"), method = Array(RequestMethod.GET))
def get(@PathVariable transferId : String) = {
val f = eventStore.find[MoneyTransfer](EntityId(transferId))
WebUtil.toDeferredResult(f map(transactionAggregate => GetMoneyTransferResponse(transferId, transactionAggregate.entity.state.getClass.getSimpleName)))
}
}

View File

@@ -0,0 +1,62 @@
package net.chrisrichardson.eventstore.examples.bank.web
import net.chrisrichardson.eventstore.EntityId
import net.chrisrichardson.eventstore.examples.bank.queryside.AccountInfo
import net.chrisrichardson.eventstore.examples.bank.transactions.TransferStates
import net.chrisrichardson.eventstore.examples.bank.web.accounts.controllers.{GetAccountResponse, CreateAccountRequest, CreateAccountResponse}
import net.chrisrichardson.eventstore.examples.bank.web.transactions.controllers.{GetMoneyTransferResponse, MoneyTransferRequest, CreateMoneyTransferResponse}
import org.junit.runner.RunWith
import org.scalatest.FlatSpec
import org.scalatest.concurrent.Eventually._
import org.scalatest.junit.JUnitRunner
import org.scalatest.time.{Millis, Span}
import org.springframework.boot.SpringApplication
import org.springframework.web.client.RestTemplate
import org.scalatest.Matchers._
@RunWith(classOf[JUnitRunner])
class BankWebIntegrationTest extends FlatSpec {
val sa = new SpringApplication(classOf[BankingWebAppTestConfiguration])
val ctx = sa.run()
// var server = ctx.getBean(classOf[EmbeddedServletContainer])
val port = 8080
val baseUrl = s"http://localhost:$port/"
val restTemplate = ctx.getBean(classOf[RestTemplate])
implicit val reallyLongPatienceConfig = PatienceConfig(timeout = Span(10 * 1000, Millis), interval = Span(1 * 1000, Millis))
it should "create accounts and transfer money" in {
val CreateAccountResponse(fromAccountId) = restTemplate.postForEntity(s"$baseUrl/accounts", CreateAccountRequest(BigDecimal(500)), classOf[CreateAccountResponse]).getBody
val CreateAccountResponse(toAccountId) = restTemplate.postForEntity(s"$baseUrl/accounts", CreateAccountRequest(BigDecimal(100)), classOf[CreateAccountResponse]).getBody
val AccountInfo(accountId, initialBalance, _, _, _) = restTemplate.getForEntity(s"$baseUrl/accounts/" + fromAccountId, classOf[AccountInfo]).getBody
accountId should be(fromAccountId)
initialBalance should be(500*100)
val CreateMoneyTransferResponse(transactionId) = restTemplate.postForEntity(s"$baseUrl/transfers",
MoneyTransferRequest(EntityId(fromAccountId), EntityId(toAccountId), BigDecimal(150)), classOf[CreateMoneyTransferResponse]).getBody
eventually {
val AccountInfo(_, newFromAccountBalance, _, _, _) = restTemplate.getForEntity(s"$baseUrl/accounts/" + fromAccountId, classOf[AccountInfo]).getBody
newFromAccountBalance should be(350*100)
}(reallyLongPatienceConfig)
eventually {
val AccountInfo(_, newToAccountBalance, _, _, _) = restTemplate.getForEntity(s"$baseUrl/accounts/" + toAccountId, classOf[AccountInfo]).getBody
newToAccountBalance should be(250*100)
}(reallyLongPatienceConfig)
eventually {
val GetMoneyTransferResponse(_, state) = restTemplate.getForEntity(s"$baseUrl/transfers/" + transactionId, classOf[GetMoneyTransferResponse]).getBody
state should be("COMPLETED$")
}(reallyLongPatienceConfig)
}
}

View File

@@ -0,0 +1,26 @@
package net.chrisrichardson.eventstore.examples.bank.web
import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.context.annotation.{Bean, Import, Configuration}
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
import org.springframework.web.client.RestTemplate
import scala.collection.JavaConversions._
@Configuration
@Import(Array(classOf[BankingWebAppConfiguration]))
class BankingWebAppTestConfiguration {
@Bean
def restTemplate(scalaObjectMapper: ObjectMapper) = {
val restTemplate = new RestTemplate()
restTemplate.getMessageConverters foreach {
case mc: MappingJackson2HttpMessageConverter =>
mc.setObjectMapper(scalaObjectMapper)
case _ =>
}
restTemplate
}
}

View File

@@ -0,0 +1,18 @@
apply plugin: 'scala'
dependencies {
compile "org.scala-lang:scala-library:2.10.2"
compile "org.springframework.boot:spring-boot-starter-data-mongodb:$springBootVersion"
compile "net.chrisrichardson.eventstore.common:eventstore-common:$eventStoreCommonVersion"
compile "net.chrisrichardson.eventstore.client:eventstore-client-event-handling:$eventStoreClientVersion"
testCompile scalaTestDependency
testCompile group: 'junit', name: 'junit', version:'4.11'
// testCompile project(":testutil")
testCompile "net.chrisrichardson.eventstore.client:eventstore-jdbc:$eventStoreClientVersion"
//testCompile project(path: ':eventstore-dynamodb', configuration: 'tests')
}

View File

@@ -0,0 +1,42 @@
package net.chrisrichardson.eventstore.examples.bank.accounts
import net.chrisrichardson.eventstore.{PatternMatchingCommandProcessingAggregate, CommandProcessingAggregate}
import net.chrisrichardson.eventstore.examples.bank.accounts.AccountCommands.AccountCommand
case class Account(balance : BigDecimal)
extends PatternMatchingCommandProcessingAggregate[Account, AccountCommand] {
def this() = this(null)
import net.chrisrichardson.eventstore.examples.bank.accounts.AccountCommands._
def processCommand = {
case OpenAccountCommand(initialBalance) =>
Seq(AccountOpenedEvent(initialBalance))
case CreditAccountCommand(amount, transactionId) =>
Seq(AccountCreditedEvent(amount, transactionId))
case DebitAccountCommand(amount, transactionId) if amount <= balance =>
Seq(AccountDebitedEvent(amount, transactionId))
case DebitAccountCommand(amount, transactionId) =>
Seq(AccountDebitFailedDueToInsufficientFundsEvent(amount, transactionId))
}
def applyEvent = {
case AccountOpenedEvent(initialBalance) => copy(balance = initialBalance)
case AccountDebitedEvent(amount, _) => copy(balance = balance - amount)
case AccountCreditedEvent(amount, _) =>
copy(balance = balance + amount)
case AccountDebitFailedDueToInsufficientFundsEvent(amount, _) =>
this
}
}

View File

@@ -0,0 +1,17 @@
package net.chrisrichardson.eventstore.examples.bank.accounts
import net.chrisrichardson.eventstore.EntityId
import net.chrisrichardson.eventstore.Command
/**
* Created by cer on 7/16/14.
*/
object AccountCommands {
sealed trait AccountCommand extends Command
case class OpenAccountCommand(initialBalance : BigDecimal) extends AccountCommand
case class DebitAccountCommand(amount : BigDecimal, transactionId : EntityId) extends AccountCommand
case class CreditAccountCommand(amount : BigDecimal, transactionId : EntityId) extends AccountCommand
}

View File

@@ -0,0 +1,21 @@
package net.chrisrichardson.eventstore.examples.bank.accounts
import net.chrisrichardson.eventstore.EventStore
import net.chrisrichardson.eventstore.subscriptions.EnableEventHandlers
import net.chrisrichardson.utils.config.MetricRegistryConfiguration
import org.springframework.context.annotation.{Bean, Configuration, Import}
@Configuration
@Import(Array(classOf[MetricRegistryConfiguration]))
@EnableEventHandlers
class AccountConfiguration {
@Bean
def accountService(eventStore : EventStore) = new AccountService()(eventStore)
@Bean
def transferWorkflow(eventStore: EventStore): TransferWorkflowAccountHandlers = {
new TransferWorkflowAccountHandlers(eventStore)
}
}

View File

@@ -0,0 +1,19 @@
package net.chrisrichardson.eventstore.examples.bank.accounts
import net.chrisrichardson.eventstore.EntityId
import net.chrisrichardson.eventstore.Event
trait AccountChangedEvent extends Event {
val amount : BigDecimal
val transactionId : EntityId
}
case class AccountOpenedEvent(initialBalance : BigDecimal) extends Event
case class AccountCreditedEvent(amount : BigDecimal, transactionId : EntityId) extends AccountChangedEvent
case class AccountDebitedEvent(amount : BigDecimal, transactionId : EntityId) extends AccountChangedEvent
case class AccountDebitFailedDueToInsufficientFundsEvent(amount : BigDecimal, transactionId : EntityId) extends AccountChangedEvent

View File

@@ -0,0 +1,12 @@
package net.chrisrichardson.eventstore.examples.bank.accounts
import net.chrisrichardson.eventstore.EventStore
import net.chrisrichardson.eventstore.examples.bank.accounts.AccountCommands.OpenAccountCommand
import net.chrisrichardson.eventstore.util.ServiceUtil._
class AccountService(implicit eventStore : EventStore) {
def openAccount(initialBalance : BigDecimal) =
newEntity[Account] <== OpenAccountCommand(initialBalance)
}

View File

@@ -0,0 +1,26 @@
package net.chrisrichardson.eventstore.examples.bank.accounts
import net.chrisrichardson.eventstore.EventStore
import net.chrisrichardson.eventstore.examples.bank.accounts.AccountCommands.{CreditAccountCommand, DebitAccountCommand}
import net.chrisrichardson.eventstore.examples.bank.transactions.{DebitRecordedEvent, MoneyTransferCreatedEvent}
import net.chrisrichardson.eventstore.subscriptions.{EventSubscriber, CompoundEventHandler, EventHandlerMethod}
import net.chrisrichardson.eventstore.util.EventHandlingUtil._
@EventSubscriber(id = "accountEventHandlers")
class TransferWorkflowAccountHandlers(eventStore: EventStore) extends CompoundEventHandler {
implicit val es = eventStore
@EventHandlerMethod
val performDebit =
handlerForEvent[MoneyTransferCreatedEvent] { de =>
existingEntity[Account](de.event.details.fromAccountId) <==
DebitAccountCommand(de.event.details.amount, de.entityId)
}
@EventHandlerMethod
val performCredit = handlerForEvent[DebitRecordedEvent] { de =>
existingEntity[Account](de.event.details.toAccountId) <== CreditAccountCommand(de.event.details.amount, de.entityId)
}
}

View File

@@ -0,0 +1,2 @@
@net.chrisrichardson.eventstore.EventEntity(entity="net.chrisrichardson.eventstore.examples.bank.accounts.Account")
package net.chrisrichardson.eventstore.examples.bank.accounts;

View File

@@ -0,0 +1,14 @@
package net.chrisrichardson.eventstore.examples.bank.queryside
import org.springframework.data.mongodb.repository.MongoRepository
case class AccountInfo(id : String, balance : Long,
changes : java.util.List[AccountChangeInfo],
transactions : java.util.List[AccountTransactionInfo],
version : String)
case class AccountChangeInfo(changeId : String, transactionId : String, transactionType : String, amount : Long, balanceDelta: Long)
case class AccountTransactionInfo(transactionId : String, fromAccountId: String, toAccountId: String, amount : Long)
trait AccountInfoRepository extends MongoRepository[AccountInfo, String]

Some files were not shown because too many files have changed in this diff Show More