Improved module name <functional-area>-<Command|Query>....

Standalone services now use the Event Store Server (many tests still use the embedded server)
This commit is contained in:
Chris Richardson
2015-04-14 19:08:07 -07:00
parent d166c9b852
commit 2e31853ad2
150 changed files with 1237 additions and 109 deletions

2
.gitignore vendored
View File

@@ -27,3 +27,5 @@ local_developer_config
/web/web.log
*.log
*.pid

View File

@@ -4,7 +4,7 @@ This example application is the money transfer application described in my talk
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.
These components can be deployed either as separate services or packaged as a monolithic application for simplified development and testing.
# About the examples
@@ -19,12 +19,23 @@ For more information, please see the [wiki](../../wiki)
# About the event store
The application use an embedded SQL-based event store.
The application use one of two event stores:
* embedded SQL-based event store, which is great for integration tests
* event store server
# Running the tests
To run the tests you need to set the following environment variable:
To run the tests you need to set some environment variable.
First, you need to tell the query side code how to connect to MongoDB:
```
export SPRING_DATA_MONGODB_URI=mongodb://192.168.59.103/yourdb
```
Docker is a great way to run MongoDB.
For more information please see this [blog post](http://plainoldobjects.com/2015/01/14/need-to-install-mongodb-rabbitmq-or-mysql-use-docker-to-simplify-dev-and-test/).
Second, in order for the tests in accounts-command-side-service, transactions-command-side-service and accounts-query-side-service to pass you need to set some environment variables that tell the service how to connect to the Event Store server.
But don't worry. The build is configured to ignore failures for those projects

View File

@@ -0,0 +1,5 @@
mongodb:
image: dockerfile/mongodb
ports:
- "27017:27017"

View File

@@ -0,0 +1,4 @@
#! /bin/bash
docker run --link esexamplesdocker_mongodb_1:mongodb -i -t dockerfile/mongodb:latest /usr/bin/mongo --host mongodb

36
handy-curl-commands.sh Normal file
View File

@@ -0,0 +1,36 @@
#! /bin/bash -e
# Create account 1
account1=$(curl -v --data '{"initialBalance" : 500}' -H "content-type: application/json" http://localhost:8080/accounts)
# {"accountId":"0000014ae4caf314-ae7453bbb71e0000"}
curl -v http://localhost:8081/accounts/0000014ae4caf314-ae7453bbb71e0000
# {"accountId":"0000014ae4caf314-ae7453bbb71e0000","balance":50000}
# Create account 2
account2=$(curl -v --data '{"initialBalance" : 300}' -H "content-type: application/json" http://localhost:8080/accounts)
# {"accountId":"0000014ae4cc8415-ae7453bbb71e0000"}
curl -v http://localhost:8081/accounts/0000014ae4cc8415-ae7453bbb71e0000
#
transfer=$(curl -v --data '{"amount" : 150, "fromAccountId" : "0000014ae4caf314-ae7453bbb71e0000", "toAccountId" : "0000014ae4cc8415-ae7453bbb71e0000"}' -H "content-type: application/json" http://localhost:8082/transfers)
# {"moneyTransferId":"0000014ae4cef030-ae7453bbb71e0000"}

View File

@@ -1,5 +1,7 @@
This is the Java/Spring version of the Event Sourcing/CQRS money transfer example application.
# About the application
This application consists of three microservices:
* Account Service - the command side business logic for Accounts
@@ -8,25 +10,28 @@ This application consists of three microservices:
The Account Service consists of the following modules:
* commandside-backend-accounts - the Account aggregate
* commandside-web-accounts - a REST API for creating and retrieving Accounts
* accounts-command-side-backend - the Account aggregate
* accounts-command-side-web - a REST API for creating and retrieving Accounts
* accounts-command-side-service - a standalone microservice
The Money Transfer Service consists of the following modules:
* commandside-backend-transactions - the MoneyTransfer aggregate
* commandside-web-transactions - a REST API for creating and retrieving Money Transfers
* transactions-command-side-backend - the MoneyTransfer aggregate
* transactions-command-side-web - a REST API for creating and retrieving Money Transfers
* transactions-command-side-service - a standalone microservice
The Query Service consists the following modules:
* queryside-backend - MongoDB-based, denormalized view of Accounts and MoneyTransfers
* queryside-web - a REST API for querying the denormalized view
* accounts-query-side-backend - MongoDB-based, denormalized view of Accounts and MoneyTransfers
* accounts-query-side-web - a REST API for querying the denormalized view
* accounts-query-side-service - a standalone microservice
In order to be used with the embedded Event Store, the three services are currently packaged as a single monolithic web application:
# Deploying the application
* monolithic-web - all-in-one, monolithic packaging of the application
These services can be deployed either as either separate standalone services using the Event Store server, or they can be deployed as a monolithic application for simpified integration testing.
As well as the above modules there are also:
The three services can also be packaged as a single monolithic web application in order to be used with the embedded Event Store:
* common-backend - code that is shared between the command side and the query side, primarily events and value objects
* backend-integration-tests - integrations tests for the backend
* monolithic-service - all-in-one, monolithic packaging of the application

View File

@@ -1,21 +1,15 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
import net.chrisrichardson.eventstore.EventStore;
import net.chrisrichardson.eventstore.javaapi.consumer.EnableJavaEventHandlers;
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
@EnableJavaEventHandlers
public class AccountConfiguration {
@Autowired
private EventHandlerRegistrarFactory eventHandlerRegistrarFactory;
@Bean
public AccountWorkflow accountWorkflow() {
return new AccountWorkflow();

View File

@@ -1,6 +1,7 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
import net.chrisrichardson.eventstore.EntityIdentifier;
import net.chrisrichardson.eventstore.javaapi.consumer.EventHandlerContext;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.DebitRecordedEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.MoneyTransferCreatedEvent;
import net.chrisrichardson.eventstore.subscriptions.*;

View File

@@ -0,0 +1,21 @@
apply plugin: VerifyMongoDBConfigurationPlugin
apply plugin: VerifyEventStoreEnvironmentPlugin
apply plugin: 'spring-boot'
dependencies {
compile project(":accounts-command-side-web")
compile "org.springframework.boot:spring-boot-starter-web"
compile "org.springframework.boot:spring-boot-starter-actuator"
compile "net.chrisrichardson.eventstore.client:eventstore-http-stomp-client:$eventStoreClientVersion"
testCompile "org.springframework.boot:spring-boot-starter-test"
}
test {
ignoreFailures true
}

View File

@@ -0,0 +1,27 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web;
import net.chrisrichardson.eventstore.client.config.EventStoreHttpClientConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.accounts.CommandSideWebAccountsConfiguration;
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({CommandSideWebAccountsConfiguration.class, EventStoreHttpClientConfiguration.class })
@EnableAutoConfiguration
@ComponentScan
public class AccountsCommandSideServiceConfiguration {
@Bean
public HttpMessageConverters customConverters() {
HttpMessageConverter<?> additional = new MappingJackson2HttpMessageConverter();
return new HttpMessageConverters(additional);
}
}

View File

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

View File

@@ -0,0 +1,55 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web;
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;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = AccountsCommandSideServiceTestConfiguration.class)
@WebAppConfiguration
@IntegrationTest({"server.port=0", "management.port=0"})
public class AccountsCommandSideServiceIntegrationTest {
@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);
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);
}
}

View File

@@ -0,0 +1,26 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
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 java.util.Arrays;
import java.util.List;
@Configuration
@Import(AccountsCommandSideServiceConfiguration.class)
public class AccountsCommandSideServiceTestConfiguration {
@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

@@ -1,7 +1,7 @@
dependencies {
compile project(":commandside-backend-accounts")
compile project(":web-common")
compile project(":accounts-command-side-backend")
compile project(":common-web")
compile "org.springframework.boot:spring-boot-starter-web:$springBootVersion"
}

View File

@@ -1,7 +1,7 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
import net.chrisrichardson.eventstore.subscriptions.EnableEventHandlers;
import net.chrisrichardson.eventstore.javaapi.consumer.EnableJavaEventHandlers;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.MongoTemplate;
@@ -9,7 +9,7 @@ import org.springframework.data.mongodb.repository.config.EnableMongoRepositorie
@Configuration
@EnableMongoRepositories
@EnableEventHandlers
@EnableJavaEventHandlers
public class QuerySideAccountConfiguration {
@Bean

View File

@@ -0,0 +1,20 @@
apply plugin: VerifyMongoDBConfigurationPlugin
apply plugin: VerifyEventStoreEnvironmentPlugin
apply plugin: 'spring-boot'
dependencies {
compile project(":accounts-query-side-web")
compile "org.springframework.boot:spring-boot-starter-web"
compile "org.springframework.boot:spring-boot-starter-actuator"
compile "net.chrisrichardson.eventstore.client:eventstore-http-stomp-client:$eventStoreClientVersion"
testCompile "org.springframework.boot:spring-boot-starter-test"
}
test {
ignoreFailures true
}

View File

@@ -0,0 +1,27 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web;
import net.chrisrichardson.eventstore.client.config.EventStoreHttpClientConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.web.queryside.QuerySideWebConfiguration;
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({QuerySideWebConfiguration.class, EventStoreHttpClientConfiguration.class})
@EnableAutoConfiguration
@ComponentScan
public class AccountsQuerySideServiceConfiguration {
@Bean
public HttpMessageConverters customConverters() {
HttpMessageConverter<?> additional = new MappingJackson2HttpMessageConverter();
return new HttpMessageConverters(additional);
}
}

View File

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

View File

@@ -0,0 +1,67 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web;
import net.chrisrichardson.eventstore.javaexamples.banking.web.queryside.accounts.GetAccountResponse;
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 = AccountsQuerySideServiceTestConfiguration.class)
@WebAppConfiguration
@IntegrationTest({"server.port=0", "management.port=0"})
public class AccountsQuerySideServiceIntegrationTest {
@Value("${local.server.port}")
private int port;
private String baseUrl(String path) {
return "http://localhost:" + port + "/" + path;
}
@Autowired
RestTemplate restTemplate;
@Test
public void shouldCreateAccountsAndTransferMoney() {
// TBD
}
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,26 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
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 java.util.Arrays;
import java.util.List;
@Configuration
@Import(AccountsQuerySideServiceConfiguration.class)
public class AccountsQuerySideServiceTestConfiguration {
@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

@@ -1,7 +1,7 @@
dependencies {
compile project(":queryside-backend")
compile project(":web-common")
compile project(":accounts-query-side-backend")
compile project(":common-web")
compile "org.springframework.boot:spring-boot-starter-web:$springBootVersion"
compile "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"

View File

@@ -2,12 +2,12 @@ apply plugin: VerifyMongoDBConfigurationPlugin
dependencies {
testCompile project(":commandside-backend-accounts")
testCompile project(":commandside-backend-transactions")
testCompile project(":queryside-backend")
testCompile project(":accounts-command-side-backend")
testCompile project(":transactions-command-side-backend")
testCompile project(":accounts-query-side-backend")
testCompile project(":testutil")
testCompile "junit:junit:4.11"
testCompile "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
testCompile "net.chrisrichardson.eventstore.client:eventstore-jdbc:$eventStoreClientVersion"
}
}

View File

@@ -24,5 +24,6 @@ subprojects {
repositories {
mavenCentral()
maven { url "https://06c59145-4e83-4f22-93ef-6a7eee7aebaa.repos.chrisrichardson.net.s3.amazonaws.com" }
}
}

View File

@@ -0,0 +1,16 @@
import org.gradle.api.*
class VerifyEventStoreEnvironmentPlugin implements Plugin<Project> {
void apply(Project project) {
project.test {
beforeSuite { x ->
if (x.parent == null) {
if (System.getenv("EVENT_STORE_URL") == null)
logger.warn("\nPLEASE make sure that Event Store-related environment variables including EVENT_STORE_URL are set, see sample-set-remote-env.sh !!!!\n")
}
}
}
}
}

View File

@@ -0,0 +1,21 @@
apply plugin: VerifyMongoDBConfigurationPlugin
dependencies {
compile "org.scala-lang:scala-library:2.10.2"
testCompile project(":accounts-command-side-web")
testCompile project(":transactions-command-side-web")
testCompile project(":accounts-query-side-web")
testCompile "junit:junit:4.11"
testCompile "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
testCompile scalaTestDependency
}
test {
ignoreFailures true
}

View File

@@ -0,0 +1,101 @@
package net.chrisrichardson.eventstore.examples.bank.web;
import net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.accounts.CreateAccountRequest;
import net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.accounts.CreateAccountResponse;
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.json.EventStoreCommonObjectMapping;
import net.chrisrichardson.eventstorestore.javaexamples.testutil.Producer;
import net.chrisrichardson.eventstorestore.javaexamples.testutil.Verifier;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import java.math.BigDecimal;
import static net.chrisrichardson.eventstorestore.javaexamples.testutil.TestUtil.eventually;
public class EndToEndTest {
private String accountsCommandSideBaseUrl(String path) {
return "http://localhost:8080/" + path;
}
private String accountsQuerySideBaseUrl(String path) {
return "http://localhost:8081/" + path;
}
private String transactionsCommandSideBaseUrl(String path) {
return "http://localhost:8082/" + path;
}
RestTemplate restTemplate = new RestTemplate();
{
for (HttpMessageConverter<?> mc : restTemplate.getMessageConverters()) {
if (mc instanceof MappingJackson2HttpMessageConverter) {
((MappingJackson2HttpMessageConverter) mc).setObjectMapper(EventStoreCommonObjectMapping.getObjectMapper());
}
}
}
@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(accountsCommandSideBaseUrl("/accounts"), new CreateAccountRequest(initialFromAccountBalance), CreateAccountResponse.class).getBody();
final String fromAccountId = fromAccount.getAccountId();
CreateAccountResponse toAccount = restTemplate.postForEntity(accountsCommandSideBaseUrl("/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(transactionsCommandSideBaseUrl("/transfers"),
new CreateMoneyTransferRequest(fromAccountId, toAccountId, amountToTransfer), CreateMoneyTransferResponse.class).getBody();
assertAccountBalance(fromAccountId, finalFromAccountBalance);
assertAccountBalance(toAccountId, finalToAccountBalance);
// TOOD - check state of money transfer
}
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(accountsQuerySideBaseUrl("/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

@@ -5,5 +5,5 @@ scalaTestDependency=org.scalatest:scalatest_2.10:2.0
springBootVersion=1.1.10.RELEASE
eventStoreCommonVersion=0.2
eventStoreClientVersion=0.2
eventStoreClientVersion=0.5
eventStoreCommonVersion=0.5

View File

@@ -3,9 +3,9 @@ apply plugin: VerifyMongoDBConfigurationPlugin
apply plugin: 'spring-boot'
dependencies {
compile project(":queryside-web")
compile project(":commandside-web-accounts")
compile project(":commandside-web-transactions")
compile project(":accounts-query-side-web")
compile project(":accounts-command-side-web")
compile project(":transactions-command-side-web")
compile "org.springframework.boot:spring-boot-starter-web"
compile "org.springframework.boot:spring-boot-starter-actuator"

View File

@@ -1,20 +1,26 @@
include 'testutil'
include 'web-common'
include 'common-web'
include 'common-backend'
include 'commandside-backend-accounts'
include 'commandside-backend-transactions'
include 'commandside-web-accounts'
include 'commandside-web-transactions'
include 'accounts-command-side-backend'
include 'transactions-command-side-backend'
include 'accounts-command-side-web'
include 'transactions-command-side-web'
include 'queryside-backend'
include 'queryside-web'
include 'accounts-query-side-backend'
include 'accounts-query-side-web'
include 'backend-integration-tests'
include 'monolithic-web'
include 'monolithic-service'
include 'accounts-command-side-service'
include 'accounts-query-side-service'
include 'transactions-command-side-service'
include 'e2e-test'
rootProject.name = 'java-spring-event-sourcing-example'

View File

@@ -12,4 +12,4 @@ dependencies {
testCompile "net.chrisrichardson.eventstore.client:eventstore-jdbc:$eventStoreClientVersion"
}
}

View File

@@ -7,10 +7,11 @@ import net.chrisrichardson.eventstore.subscriptions.config.EventStoreSubscriptio
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import net.chrisrichardson.eventstore.javaapi.consumer.EnableJavaEventHandlers;
@Configuration
@Import(EventStoreSubscriptionsConfiguration.class)
@EnableEventHandlers
@EnableJavaEventHandlers
public class MoneyTransferConfiguration {
@Bean

View File

@@ -1,11 +1,11 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions;
import net.chrisrichardson.eventstore.javaapi.consumer.EventHandlerContext;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountCreditedEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountDebitFailedDueToInsufficientFundsEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.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;

View File

@@ -0,0 +1,20 @@
apply plugin: 'spring-boot'
apply plugin: VerifyEventStoreEnvironmentPlugin
dependencies {
compile project(":transactions-command-side-web")
compile "org.springframework.boot:spring-boot-starter-web"
compile "org.springframework.boot:spring-boot-starter-actuator"
compile "net.chrisrichardson.eventstore.client:eventstore-http-stomp-client:$eventStoreClientVersion"
testCompile "org.springframework.boot:spring-boot-starter-test"
}
test {
ignoreFailures true
}

View File

@@ -0,0 +1,27 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web;
import net.chrisrichardson.eventstore.client.config.EventStoreHttpClientConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.transactions.CommandSideWebTransactionsConfiguration;
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({CommandSideWebTransactionsConfiguration.class, EventStoreHttpClientConfiguration.class})
@EnableAutoConfiguration
@ComponentScan
public class TransactionsCommandSideServiceConfiguration {
@Bean
public HttpMessageConverters customConverters() {
HttpMessageConverter<?> additional = new MappingJackson2HttpMessageConverter();
return new HttpMessageConverters(additional);
}
}

View File

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

View File

@@ -0,0 +1,35 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web;
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;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = TransactionsCommandSideServiceTestConfiguration.class)
@WebAppConfiguration
@IntegrationTest({"server.port=0", "management.port=0"})
public class TransactionsCommandSideServiceIntegrationTest {
@Value("${local.server.port}")
private int port;
private String baseUrl(String path) {
return "http://localhost:" + port + "/" + path;
}
@Autowired
RestTemplate restTemplate;
@Test
public void shouldCreateAccountsAndTransferMoney() {
// TBD
}
}

View File

@@ -0,0 +1,26 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
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 java.util.Arrays;
import java.util.List;
@Configuration
@Import(TransactionsCommandSideServiceConfiguration.class)
public class TransactionsCommandSideServiceTestConfiguration {
@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

@@ -1,7 +1,7 @@
dependencies {
compile project(":commandside-backend-transactions")
compile project(":web-common")
compile project(":transactions-command-side-backend")
compile project(":common-web")
compile "org.springframework.boot:spring-boot-starter-web:$springBootVersion"
}

4
kill-all-services.sh Executable file
View File

@@ -0,0 +1,4 @@
#! /bin/bash
kill `cat account-cs.pid account-qs.pid transfers-cs.pid`
rm account-cs.pid account-qs.pid transfers-cs.pid

31
run-all-services.sh Executable file
View File

@@ -0,0 +1,31 @@
#! /bin/bash -e
# Execute this script in the java-spring or scala-spring directory
# Runs all of the services
if [[ -f account-cs.pid ]]; then
echo pid file exists
exit 1
fi
java -jar accounts-command-side-service/build/libs/accounts-command-side-service.jar > account-cs.log &
echo $! > account-cs.pid
java -jar accounts-query-side-service/build/libs/accounts-query-side-service.jar --server.port=8081 > account-qs.log &
echo $! > account-qs.pid
java -jar transactions-command-side-service/build/libs/transactions-command-side-service.jar --server.port=8082 > transfers-cs.log &
echo $! > transfers-cs.pid
echo -n waiting for services....
while [[ true ]]; do
nc -z -w 4 localhost 8080 && nc -z -w 4 localhost 8081 && nc -z -w 4 localhost 8082
if [[ "$?" -eq "0" ]]; then
echo connected
break
fi
echo -n .
sleep 1
done

6
run-e2e-test-all.sh Executable file
View File

@@ -0,0 +1,6 @@
#! /bin/bash -e
for dir in java-spring scala-spring; do
(cd $dir ; ../run-e2e-test.sh)
done

16
run-e2e-test.sh Executable file
View File

@@ -0,0 +1,16 @@
#! /bin/bash -e
# Must be run in the java-spring or scala-spring directories
echo starting services
../run-all-services.sh
echo running test
./gradlew :e2e-test:cleanTest :e2e-test:test
echo killing services
../kill-all-services.sh

9
sample-set-server-env.sh Executable file
View File

@@ -0,0 +1,9 @@
#! /bin/bash -e
export EVENT_STORE_USER_ID=Aladdin
export EVENT_STORE_PASSWORD="open sesame"
export EVENT_STORE_URL=serverUrl
export EVENT_STORE_STOMP_SERVER_HOST=serverhost
export EVENT_STORE_STOMP_SERVER_PORT=serverPort
export SPRING_DATA_MONGODB_URI=mongodb://192.168.59.103/mydb

View File

@@ -1,5 +1,7 @@
This is the Scala/Spring version of the Event Sourcing/CQRS money transfer example application.
# About the application
This application consists of three microservices:
* Account Service - the command side business logic for Accounts
@@ -8,25 +10,28 @@ This application consists of three microservices:
The Account Service consists of the following modules:
* commandside-backend-accounts - the Account aggregate
* commandside-web-accounts - a REST API for creating and retrieving Accounts
* accounts-command-side-backend - the Account aggregate
* accounts-command-side-web - a REST API for creating and retrieving Accounts
* accounts-command-side-service - a standalone microservice
The Money Transfer Service consists of the following modules:
* commandside-backend-transactions - the MoneyTransfer aggregate
* commandside-web-transactions - a REST API for creating and retrieving Money Transfers
* transactions-command-side-backend - the MoneyTransfer aggregate
* transactions-command-side-web - a REST API for creating and retrieving Money Transfers
* transactions-command-side-service - a standalone microservice
The Query Service consists the following modules:
* queryside-backend - MongoDB-based, denormalized view of Accounts and MoneyTransfers
* queryside-web - a REST API for querying the denormalized view
* accounts-query-side-backend - MongoDB-based, denormalized view of Accounts and MoneyTransfers
* accounts-query-side-web - a REST API for querying the denormalized view
* accounts-query-side-service - a standalone microservice
In order to be used with the embedded Event Store, the three services are currently packaged as a single monolithic web application:
# Deploying the application
* monolithic-web - all-in-one, monolithic packaging of the application
These services can be deployed either as either separate standalone services using the Event Store server, or they can be deployed as a monolithic application for simpified integration testing.
As well as the above modules there are also:
The three services can also be packaged as a single monolithic web application in order to be used with the embedded Event Store:
* common-backend - code that is shared between the command side and the query side, primarily events and value objects
* backend-integration-tests - integrations tests for the backend
* monolithic-service - all-in-one, monolithic packaging of the application

View File

@@ -2,4 +2,4 @@ dependencies {
compile "org.scala-lang:scala-library:2.10.2"
compile project(":common-backend")
compile "net.chrisrichardson.eventstore.client:eventstore-client-event-handling:$eventStoreClientVersion"
}
}

View File

@@ -20,7 +20,8 @@ class TransferWorkflowAccountHandlers(eventStore: EventStore) extends CompoundEv
@EventHandlerMethod
val performCredit = handlerForEvent[DebitRecordedEvent] { de =>
existingEntity[Account](de.event.details.toAccountId) <== CreditAccountCommand(de.event.details.amount, de.entityId)
existingEntity[Account](de.event.details.toAccountId) <==
CreditAccountCommand(de.event.details.amount, de.entityId)
}
}

View File

@@ -0,0 +1,22 @@
apply plugin: 'scala'
apply plugin: 'spring-boot'
apply plugin: VerifyEventStoreEnvironmentPlugin
dependencies {
compile "org.scala-lang:scala-library:2.10.2"
compile project(":accounts-command-side-web")
compile "org.springframework.boot:spring-boot-starter-web"
compile "org.springframework.boot:spring-boot-starter-actuator"
compile "net.chrisrichardson.eventstore.client:eventstore-http-stomp-client:$eventStoreClientVersion"
testCompile "org.springframework.boot:spring-boot-starter-test"
testCompile scalaTestDependency
}
test {
ignoreFailures true
}

View File

@@ -0,0 +1,14 @@
package net.chrisrichardson.eventstore.examples.bank.web
import net.chrisrichardson.eventstore.client.config.EventStoreHttpClientConfiguration
import net.chrisrichardson.eventstore.examples.bank.web.accounts.CommandSideWebAccountsConfiguration
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.context.annotation._
@Configuration
@EnableAutoConfiguration
@Import(Array(classOf[CommandSideWebAccountsConfiguration], classOf[EventStoreHttpClientConfiguration]))
@ComponentScan
class AccountsCommandSideServiceConfiguration {
}

View File

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

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