├── .gitignore ├── pom.xml ├── readme.md └── src ├── main └── java │ └── app │ ├── config │ ├── ApplicationConfig.java │ └── MongoDBConfig.java │ ├── domain │ ├── Account.java │ ├── Entity.java │ ├── Transaction.java │ └── TransactionState.java │ ├── repository │ ├── AccountRepository.java │ └── TransactionRepository.java │ └── service │ ├── AccountService.java │ ├── TransactionService.java │ └── TransferService.java └── test └── java └── app └── MongoDBTwoPhaseCommitsTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .gitignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 4 | 5 | ## Directory-based project format 6 | .idea/ 7 | # if you remove the above rule, at least ignore user-specific stuff: 8 | # .idea/workspace.xml 9 | # .idea/tasks.xml 10 | # and these sensitive or high-churn files: 11 | # .idea/dataSources.ids 12 | # .idea/dataSources.xml 13 | # .idea/sqlDataSources.xml 14 | # .idea/dynamic.xml 15 | 16 | ## File-based project format 17 | *.ipr 18 | *.iml 19 | *.iws 20 | 21 | ## Additional for IntelliJ 22 | out/ 23 | 24 | # generated by mpeltonen/sbt-idea plugin 25 | .idea_modules/ 26 | 27 | # generated by JIRA plugin 28 | atlassian-ide-plugin.xml 29 | 30 | # generated by Crashlytics plugin (for Android Studio and Intellij) 31 | com_crashlytics_export_strings.xml 32 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | app 6 | app 7 | 1.0-SNAPSHOT 8 | jar 9 | 10 | app 11 | 12 | 13 | UTF-8 14 | 2.12.3 15 | 4.11 16 | 1.18 17 | 1.3 18 | 1.1 19 | 4.0.6.RELEASE 20 | 21 | 22 | 23 | 24 | 25 | org.springframework 26 | spring-beans 27 | ${spring.version} 28 | 29 | 30 | 31 | org.springframework 32 | spring-context 33 | ${spring.version} 34 | 35 | 36 | 37 | org.springframework 38 | spring-test 39 | ${spring.version} 40 | 41 | 42 | 43 | org.mongodb 44 | mongo-java-driver 45 | ${mongo-java-driver.version} 46 | 47 | 48 | 49 | org.jongo 50 | jongo 51 | ${jongo.version} 52 | 53 | 54 | 55 | de.flapdoodle.embedmongo 56 | de.flapdoodle.embedmongo 57 | ${de.flapdoodle.embedmongo.version} 58 | 59 | 60 | 61 | junit 62 | junit 63 | ${junit.version} 64 | test 65 | 66 | 67 | 68 | org.hamcrest 69 | hamcrest-all 70 | ${hamcrest-all.version} 71 | test 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | org.apache.maven.plugins 80 | maven-compiler-plugin 81 | 3.1 82 | 83 | 1.7 84 | 1.7 85 | 86 | 87 | 88 | org.apache.maven.plugins 89 | maven-jar-plugin 90 | 2.5 91 | 92 | 93 | org.codehaus.mojo 94 | versions-maven-plugin 95 | 2.1 96 | 97 | false 98 | false 99 | 100 | 101 | 102 | org.apache.maven.plugins 103 | maven-pmd-plugin 104 | 3.2 105 | 106 | 107 | compile 108 | 109 | check 110 | cpd-check 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | **Note:** 2 | - Code has a demo purpose. Some operations are performed for a better understanding, other operations are not performed to not create confusion. 3 | - See test class for testing scenarios. Some tests are not implemented. 4 | 5 | **Resource:** http://docs.mongodb.org/manual/tutorial/perform-two-phase-commits/ 6 | 7 | Consider a scenario where you want to transfer funds from account A to account B. In a relational database system, you can subtract the funds from A and add the funds to B in a single multi-statement transaction. In MongoDB, you can emulate a two-phase commit to achieve a comparable result. 8 | 9 | The examples in this tutorial use the following two collections: 10 | 11 | - A collection named accounts to store account information. 12 | - A collection named transactions to store information on the fund transfer transactions. -------------------------------------------------------------------------------- /src/main/java/app/config/ApplicationConfig.java: -------------------------------------------------------------------------------- 1 | package app.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.ComponentScan; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.context.annotation.Import; 7 | import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; 8 | 9 | @Configuration 10 | @Import(MongoDBConfig.class) 11 | @ComponentScan(basePackages = {"app.repository", "app.service"}) 12 | public class ApplicationConfig { 13 | 14 | @Bean 15 | public static PropertySourcesPlaceholderConfigurer propertyPlaceholderConfigurer() { 16 | return new PropertySourcesPlaceholderConfigurer(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/app/config/MongoDBConfig.java: -------------------------------------------------------------------------------- 1 | package app.config; 2 | 3 | import com.mongodb.DB; 4 | import com.mongodb.Mongo; 5 | import com.mongodb.MongoClient; 6 | import com.mongodb.WriteConcern; 7 | import de.flapdoodle.embedmongo.MongoDBRuntime; 8 | import de.flapdoodle.embedmongo.MongodExecutable; 9 | import de.flapdoodle.embedmongo.MongodProcess; 10 | import de.flapdoodle.embedmongo.config.MongodConfig; 11 | import de.flapdoodle.embedmongo.distribution.Version; 12 | import de.flapdoodle.embedmongo.runtime.Network; 13 | import org.jongo.Jongo; 14 | import org.springframework.beans.factory.DisposableBean; 15 | import org.springframework.beans.factory.InitializingBean; 16 | import org.springframework.beans.factory.annotation.Value; 17 | import org.springframework.context.annotation.Bean; 18 | import org.springframework.context.annotation.Configuration; 19 | 20 | @Configuration 21 | public class MongoDBConfig implements InitializingBean, DisposableBean { 22 | 23 | @Value("${mongo.host:localhost}") 24 | private String mongoHost; 25 | 26 | @Value("${mongo.port:17017}") 27 | private Integer mongoPort; 28 | 29 | @Value("${mongo.db:database}") 30 | private String mongoDatabase; 31 | 32 | private MongodExecutable mongodExe; 33 | private MongodProcess mongod; 34 | private Mongo mongo; 35 | private DB db; 36 | 37 | @Bean 38 | public Jongo jongo() throws Exception { 39 | return new Jongo(db); 40 | } 41 | 42 | @Override 43 | public void afterPropertiesSet() throws Exception { 44 | MongoDBRuntime runtime = MongoDBRuntime.getDefaultInstance(); 45 | mongodExe = runtime.prepare(new MongodConfig(Version.V2_2_0_RC0, mongoPort, Network.localhostIsIPv6())); 46 | mongod = mongodExe.start(); 47 | 48 | mongo = new MongoClient(mongoHost, mongoPort); 49 | mongo.setWriteConcern(WriteConcern.ACKNOWLEDGED); 50 | 51 | db = mongo.getDB(mongoDatabase); 52 | } 53 | 54 | @Override 55 | public void destroy() throws Exception { 56 | db.dropDatabase(); 57 | mongo.close(); 58 | mongod.stop(); 59 | mongodExe.cleanup(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/app/domain/Account.java: -------------------------------------------------------------------------------- 1 | package app.domain; 2 | 3 | public class Account extends Entity { 4 | 5 | private Integer balance; 6 | private Object[] pendingTransactions; 7 | 8 | public Integer getBalance() { 9 | return balance; 10 | } 11 | 12 | public void setBalance(Integer balance) { 13 | this.balance = balance; 14 | } 15 | 16 | public Object[] getPendingTransactions() { 17 | return pendingTransactions; 18 | } 19 | 20 | public void setPendingTransactions(Object[] pendingTransactions) { 21 | this.pendingTransactions = pendingTransactions; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/app/domain/Entity.java: -------------------------------------------------------------------------------- 1 | package app.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public class Entity { 6 | 7 | @JsonProperty("_id") 8 | private String id; 9 | 10 | public String getId() { 11 | return id; 12 | } 13 | 14 | public void setId(String id) { 15 | this.id = id; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/app/domain/Transaction.java: -------------------------------------------------------------------------------- 1 | package app.domain; 2 | 3 | public class Transaction extends Entity { 4 | 5 | private String source; 6 | private String destination; 7 | private int value; 8 | private TransactionState state; 9 | private long lastModified; 10 | 11 | public String getSource() { 12 | return source; 13 | } 14 | 15 | public void setSource(String source) { 16 | this.source = source; 17 | } 18 | 19 | public String getDestination() { 20 | return destination; 21 | } 22 | 23 | public void setDestination(String destination) { 24 | this.destination = destination; 25 | } 26 | 27 | public int getValue() { 28 | return value; 29 | } 30 | 31 | public void setValue(int value) { 32 | this.value = value; 33 | } 34 | 35 | public long getLastModified() { 36 | return lastModified; 37 | } 38 | 39 | public void setLastModified(long lastModified) { 40 | this.lastModified = lastModified; 41 | } 42 | 43 | public TransactionState getState() { 44 | return state; 45 | } 46 | 47 | public void setState(TransactionState state) { 48 | this.state = state; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/app/domain/TransactionState.java: -------------------------------------------------------------------------------- 1 | package app.domain; 2 | 3 | public enum TransactionState { 4 | INITIAL, 5 | PENDING, 6 | APPLIED, 7 | DONE, 8 | CANCELING, 9 | CANCELED 10 | }; -------------------------------------------------------------------------------- /src/main/java/app/repository/AccountRepository.java: -------------------------------------------------------------------------------- 1 | package app.repository; 2 | 3 | import org.jongo.Jongo; 4 | import org.jongo.MongoCollection; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Repository; 7 | 8 | @Repository 9 | public class AccountRepository { 10 | 11 | private MongoCollection accounts; 12 | 13 | @Autowired 14 | public AccountRepository(Jongo jongo) { 15 | this.accounts = jongo.getCollection("accounts"); 16 | } 17 | 18 | public void insert(String id, int balance, Object[] pendingTransactions) { 19 | accounts.insert( 20 | "{_id: #, balance: #, pendingTransactions: #}", id, balance, pendingTransactions 21 | ); 22 | } 23 | 24 | public void updateBalanceAndPushToPendingTransactions(String accountId, int amount, String transactionId) { 25 | accounts.update( 26 | "{ _id: #, pendingTransactions: { $ne: #}},", accountId, transactionId 27 | ).with( 28 | "{ $inc: { balance: #}, $push: { pendingTransactions: #}}", amount, transactionId 29 | ); 30 | } 31 | 32 | public void updateBalanceAndPullFromPendingTransactions(String accountId, int amount, String transactionId) { 33 | accounts.update( 34 | "{ _id: #, pendingTransactions: #},", accountId, transactionId 35 | ).with( 36 | "{ $inc: { balance: #}, $pull: { pendingTransactions: #}}", amount, transactionId 37 | ); 38 | } 39 | 40 | public void updatePullFromPendingTransactions(String accountId, String transactionId) { 41 | accounts.update( 42 | "{ _id: #, pendingTransactions: #},", accountId, transactionId 43 | ).with( 44 | "{ $pull: { pendingTransactions: #}}", transactionId 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/app/repository/TransactionRepository.java: -------------------------------------------------------------------------------- 1 | package app.repository; 2 | 3 | import app.domain.Transaction; 4 | import app.domain.TransactionState; 5 | import org.jongo.Jongo; 6 | import org.jongo.MongoCollection; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Repository; 9 | 10 | @Repository 11 | public class TransactionRepository { 12 | 13 | private MongoCollection transactions; 14 | 15 | @Autowired 16 | public TransactionRepository(Jongo jongo) { 17 | this.transactions = jongo.getCollection("transactions"); 18 | } 19 | 20 | public void insert(String transactionId, String source, String destination, int value, TransactionState state) { 21 | transactions.insert( 22 | "{ _id: #, source: #, destination: #, value: #, state: #, lastModified: #}", transactionId, source, destination, value, state, System.currentTimeMillis() 23 | ); 24 | } 25 | 26 | public void updateState(String transactionId, TransactionState fromState, TransactionState toState) { 27 | transactions.update( 28 | "{ _id: #, state: #}", transactionId, fromState 29 | ).with( 30 | "{$set: { state: #, lastModified: #}}", toState, System.currentTimeMillis() 31 | ); 32 | } 33 | 34 | public Transaction findTransactionByStateAndLastModified(TransactionState state, long dateThreshold) { 35 | return transactions.findOne( 36 | "{state: #, lastModified: {$lt: #}}", state, dateThreshold 37 | ).as(Transaction.class); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/app/service/AccountService.java: -------------------------------------------------------------------------------- 1 | package app.service; 2 | 3 | import app.repository.AccountRepository; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.stereotype.Service; 6 | 7 | @Service 8 | public class AccountService { 9 | 10 | @Autowired 11 | private AccountRepository repository; 12 | 13 | public void insert(String accountId, int balance, Object[] pendingTransactions) { 14 | repository.insert(accountId, balance, pendingTransactions); 15 | } 16 | 17 | public void updateBalanceAndPushToPendingTransactions(String accountId, int amount, String transactionId) { 18 | repository.updateBalanceAndPushToPendingTransactions(accountId, amount, transactionId); 19 | } 20 | 21 | public void updateBalanceAndPullFromPendingTransactions(String accountId, int amount, String transactionId) { 22 | repository.updateBalanceAndPullFromPendingTransactions(accountId, amount, transactionId); 23 | } 24 | 25 | public void updatePullFromPendingTransactions(String accountId, String transactionId) { 26 | repository.updatePullFromPendingTransactions(accountId, transactionId); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/app/service/TransactionService.java: -------------------------------------------------------------------------------- 1 | package app.service; 2 | 3 | import app.domain.Transaction; 4 | import app.domain.TransactionState; 5 | import app.repository.TransactionRepository; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Service; 8 | 9 | @Service 10 | public class TransactionService { 11 | 12 | @Autowired 13 | private TransactionRepository repository; 14 | 15 | private long MINUTES_IN_MILLISECONDS = 30 * 60 * 1000; 16 | 17 | public void insert(String transactionId, String source, String destination, int value, TransactionState state) { 18 | repository.insert(transactionId, source, destination, value, state); 19 | } 20 | 21 | public void updateState(String transactionId, TransactionState fromState, TransactionState toState) { 22 | repository.updateState(transactionId, fromState, toState); 23 | } 24 | 25 | public Transaction findTransactionByStateAndLastModified(TransactionState state) { 26 | long dateThreshold = System.currentTimeMillis() - MINUTES_IN_MILLISECONDS; 27 | 28 | return repository.findTransactionByStateAndLastModified(state, dateThreshold); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/app/service/TransferService.java: -------------------------------------------------------------------------------- 1 | package app.service; 2 | 3 | import app.domain.Transaction; 4 | import app.domain.TransactionState; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Service; 7 | 8 | @Service 9 | @SuppressWarnings("CPD-START") 10 | public class TransferService { 11 | 12 | @Autowired 13 | private TransactionService transactionService; 14 | 15 | @Autowired 16 | private AccountService accountService; 17 | 18 | public void transfer(Transaction transaction) { 19 | 20 | transactionService.updateState(transaction.getId(), TransactionState.INITIAL, TransactionState.PENDING); 21 | 22 | accountService.updateBalanceAndPushToPendingTransactions(transaction.getSource(), -transaction.getValue(), transaction.getId()); 23 | accountService.updateBalanceAndPushToPendingTransactions(transaction.getDestination(), transaction.getValue(), transaction.getId()); 24 | 25 | transactionService.updateState(transaction.getId(), TransactionState.PENDING, TransactionState.APPLIED); 26 | 27 | accountService.updatePullFromPendingTransactions(transaction.getSource(), transaction.getId()); 28 | accountService.updatePullFromPendingTransactions(transaction.getDestination(), transaction.getId()); 29 | 30 | transactionService.updateState(transaction.getId(), TransactionState.APPLIED, TransactionState.DONE); 31 | } 32 | 33 | public void recoverPending(Transaction transaction) { 34 | 35 | accountService.updateBalanceAndPushToPendingTransactions(transaction.getSource(), -transaction.getValue(), transaction.getId()); 36 | accountService.updateBalanceAndPushToPendingTransactions(transaction.getDestination(), transaction.getValue(), transaction.getId()); 37 | 38 | transactionService.updateState(transaction.getId(), TransactionState.PENDING, TransactionState.APPLIED); 39 | 40 | accountService.updatePullFromPendingTransactions(transaction.getSource(), transaction.getId()); 41 | accountService.updatePullFromPendingTransactions(transaction.getDestination(), transaction.getId()); 42 | 43 | transactionService.updateState(transaction.getId(), TransactionState.APPLIED, TransactionState.DONE); 44 | } 45 | 46 | public void recoverApplied(Transaction transaction) { 47 | 48 | accountService.updatePullFromPendingTransactions(transaction.getSource(), transaction.getId()); 49 | accountService.updatePullFromPendingTransactions(transaction.getDestination(), transaction.getId()); 50 | 51 | transactionService.updateState(transaction.getId(), TransactionState.APPLIED, TransactionState.DONE); 52 | } 53 | 54 | public void cancelPending(Transaction transaction) { 55 | 56 | transactionService.updateState(transaction.getId(), TransactionState.PENDING, TransactionState.CANCELING); 57 | 58 | accountService.updateBalanceAndPullFromPendingTransactions(transaction.getSource(), transaction.getValue(), transaction.getId()); 59 | accountService.updateBalanceAndPullFromPendingTransactions(transaction.getDestination(), -transaction.getValue(), transaction.getId()); 60 | 61 | transactionService.updateState(transaction.getId(), TransactionState.CANCELING, TransactionState.CANCELED); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/app/MongoDBTwoPhaseCommitsTest.java: -------------------------------------------------------------------------------- 1 | package app; 2 | 3 | import app.config.ApplicationConfig; 4 | import app.domain.Account; 5 | import app.domain.Transaction; 6 | import app.domain.TransactionState; 7 | import app.service.AccountService; 8 | import app.service.TransactionService; 9 | import app.service.TransferService; 10 | import org.jongo.Jongo; 11 | import org.jongo.MongoCollection; 12 | import org.junit.After; 13 | import org.junit.Before; 14 | import org.junit.Test; 15 | import org.junit.runner.RunWith; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.test.context.ContextConfiguration; 18 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 19 | 20 | import static org.hamcrest.CoreMatchers.is; 21 | import static org.hamcrest.MatcherAssert.assertThat; 22 | import static org.hamcrest.Matchers.emptyArray; 23 | 24 | @RunWith(SpringJUnit4ClassRunner.class) 25 | @ContextConfiguration(classes={ApplicationConfig.class}) 26 | public class MongoDBTwoPhaseCommitsTest { 27 | 28 | @Autowired 29 | private AccountService accountService; 30 | 31 | @Autowired 32 | private TransactionService transactionService; 33 | 34 | @Autowired 35 | private TransferService transferService; 36 | 37 | @Autowired 38 | private Jongo jongo; 39 | 40 | private MongoCollection accounts; 41 | private MongoCollection transactions; 42 | 43 | private long HOUR_IN_MILLISECONDS = 60 * 60 * 1000; 44 | 45 | @Before 46 | public void setUp() throws Exception { 47 | accounts = jongo.getCollection("accounts"); 48 | transactions = jongo.getCollection("transactions"); 49 | } 50 | 51 | @After 52 | public void tearDown() throws Exception { 53 | accounts.drop(); 54 | transactions.drop(); 55 | } 56 | 57 | @Test 58 | public void testBasicSetup() throws Exception { 59 | 60 | accountService.insert("A", 1000, new Object[0]); 61 | 62 | Account retrievedAccount = accounts.findOne().as(Account.class); 63 | 64 | assertThat(retrievedAccount.getBalance(), is(1000)); 65 | assertThat(retrievedAccount.getPendingTransactions(), is(new Object[0])); 66 | } 67 | 68 | @Test 69 | public void testInitialVersion() throws Exception { 70 | 71 | accounts.insert( 72 | "[" + 73 | " { _id: \"A\", balance: 1000, pendingTransactions: [] },\n" + 74 | " { _id: \"B\", balance: 1000, pendingTransactions: [] }\n" + 75 | "]" 76 | ); 77 | 78 | transactions.insert( 79 | "{ _id: \"1\", source: \"A\", destination: \"B\", value: 100, state: #, lastModified: #}", TransactionState.INITIAL, System.currentTimeMillis() 80 | ); 81 | 82 | //Retrieve the transaction to start. 83 | Transaction transaction = transactions.findOne().as(Transaction.class); 84 | 85 | //Update transaction state to pending. 86 | transactions.update( 87 | "{ _id: #, state: #}", transaction.getId(), TransactionState.INITIAL 88 | ).with( 89 | "{$set: { state: #, lastModified: #}}", TransactionState.PENDING, System.currentTimeMillis() 90 | ); 91 | 92 | //Apply the transaction to both accounts. 93 | accounts.update( 94 | "{ _id: #, pendingTransactions: { $ne: #}},", transaction.getSource(), transaction.getId() 95 | ).with( 96 | "{ $inc: { balance: #}, $push: { pendingTransactions: #}}", -transaction.getValue(), transaction.getId() 97 | ); 98 | 99 | accounts.update( 100 | "{ _id: #, pendingTransactions: { $ne: #}},", transaction.getDestination(), transaction.getId() 101 | ).with( 102 | "{ $inc: { balance: #}, $push: { pendingTransactions: #}}", transaction.getValue(), transaction.getId() 103 | ); 104 | 105 | //Update transaction state to applied. 106 | transactions.update( 107 | "{ _id: #, state: #}", transaction.getId(), TransactionState.PENDING 108 | ).with( 109 | "{$set: { state: #, lastModified: #}}", TransactionState.APPLIED, System.currentTimeMillis() 110 | ); 111 | 112 | //Update both accounts’ list of pending transactions. 113 | accounts.update( 114 | "{ _id: #, pendingTransactions: #},", transaction.getSource(), transaction.getId() 115 | ).with( 116 | "{ $pull: { pendingTransactions: #}}",transaction.getId() 117 | ); 118 | 119 | accounts.update( 120 | "{ _id: #, pendingTransactions: #},", transaction.getDestination(), transaction.getId() 121 | ).with( 122 | "{ $pull: { pendingTransactions: #}}",transaction.getId() 123 | ); 124 | 125 | //Update transaction state to done. 126 | transactions.update( 127 | "{ _id: #, state: #}", transaction.getId(), TransactionState.APPLIED 128 | ).with( 129 | "{$set: { state: #, lastModified: #}}", TransactionState.DONE, System.currentTimeMillis() 130 | ); 131 | 132 | Account accountA = accounts.findOne("{_id: \"A\"}").as(Account.class); 133 | assertThat(accountA.getBalance(), is(900)); 134 | assertThat(accountA.getPendingTransactions(), is(emptyArray())); 135 | 136 | Account accountB = accounts.findOne("{_id: \"B\"}").as(Account.class); 137 | assertThat(accountB.getBalance(), is(1100)); 138 | assertThat(accountB.getPendingTransactions(), is(emptyArray())); 139 | 140 | Transaction finalTransaction = transactions.findOne().as(Transaction.class); 141 | assertThat(finalTransaction.getState(), is(TransactionState.DONE)); 142 | } 143 | 144 | @Test 145 | public void testRefactoredVersion() throws Exception { 146 | 147 | accounts.insert( 148 | "[" + 149 | " { _id: \"A\", balance: 1000, pendingTransactions: [] },\n" + 150 | " { _id: \"B\", balance: 1000, pendingTransactions: [] }\n" + 151 | "]" 152 | ); 153 | 154 | transactions.insert( 155 | "{ _id: \"1\", source: \"A\", destination: \"B\", value: 100, state: #, lastModified: #}", TransactionState.INITIAL, System.currentTimeMillis() 156 | ); 157 | 158 | Transaction transaction = transactions.findOne().as(Transaction.class); 159 | 160 | transferService.transfer(transaction); 161 | 162 | Account accountA = accounts.findOne("{_id: \"A\"}").as(Account.class); 163 | assertThat(accountA.getBalance(), is(900)); 164 | assertThat(accountA.getPendingTransactions(), is(emptyArray())); 165 | 166 | Account accountB = accounts.findOne("{_id: \"B\"}").as(Account.class); 167 | assertThat(accountB.getBalance(), is(1100)); 168 | assertThat(accountB.getPendingTransactions(), is(emptyArray())); 169 | 170 | Transaction finalTransaction = transactions.findOne().as(Transaction.class); 171 | assertThat(finalTransaction.getState(), is(TransactionState.DONE)); 172 | } 173 | 174 | @Test 175 | public void testRecoverPendingState() throws Exception { 176 | accounts.insert( 177 | "[" + 178 | " { _id: \"A\", balance: 900, pendingTransactions: [\"1\"] },\n" + 179 | " { _id: \"B\", balance: 1000, pendingTransactions: [] }\n" + 180 | "]" 181 | ); 182 | 183 | transactions.insert( 184 | "{ _id: \"1\", source: \"A\", destination: \"B\", value: 100, state: #, lastModified: #}", TransactionState.PENDING, System.currentTimeMillis() - HOUR_IN_MILLISECONDS 185 | ); 186 | 187 | Transaction transaction = transactionService.findTransactionByStateAndLastModified(TransactionState.PENDING); 188 | 189 | transferService.recoverPending(transaction); 190 | 191 | Account accountA = accounts.findOne("{_id: \"A\"}").as(Account.class); 192 | assertThat(accountA.getBalance(), is(900)); 193 | assertThat(accountA.getPendingTransactions(), is(emptyArray())); 194 | 195 | Account accountB = accounts.findOne("{_id: \"B\"}").as(Account.class); 196 | assertThat(accountB.getBalance(), is(1100)); 197 | assertThat(accountB.getPendingTransactions(), is(emptyArray())); 198 | 199 | Transaction finalTransaction = transactions.findOne().as(Transaction.class); 200 | assertThat(finalTransaction.getState(), is(TransactionState.DONE)); 201 | } 202 | 203 | @Test 204 | public void testRecoverAppliedState() throws Exception { 205 | accounts.insert( 206 | "[" + 207 | " { _id: \"A\", balance: 900, pendingTransactions: [\"1\"] },\n" + 208 | " { _id: \"B\", balance: 1100, pendingTransactions: [\"1\"] }\n" + 209 | "]" 210 | ); 211 | 212 | transactions.insert( 213 | "{ _id: \"1\", source: \"A\", destination: \"B\", value: 100, state: #, lastModified: #}", TransactionState.APPLIED, System.currentTimeMillis() - HOUR_IN_MILLISECONDS 214 | ); 215 | 216 | Transaction transaction = transactionService.findTransactionByStateAndLastModified(TransactionState.APPLIED); 217 | 218 | transferService.recoverApplied(transaction); 219 | 220 | Account accountA = accounts.findOne("{_id: \"A\"}").as(Account.class); 221 | assertThat(accountA.getBalance(), is(900)); 222 | assertThat(accountA.getPendingTransactions(), is(emptyArray())); 223 | 224 | Account accountB = accounts.findOne("{_id: \"B\"}").as(Account.class); 225 | assertThat(accountB.getBalance(), is(1100)); 226 | assertThat(accountB.getPendingTransactions(), is(emptyArray())); 227 | 228 | Transaction finalTransaction = transactions.findOne().as(Transaction.class); 229 | assertThat(finalTransaction.getState(), is(TransactionState.DONE)); 230 | } 231 | 232 | 233 | @Test 234 | public void testCancelPending() throws Exception { 235 | 236 | accounts.insert( 237 | "[" + 238 | " { _id: \"A\", balance: 900, pendingTransactions: [\"1\"] },\n" + 239 | " { _id: \"B\", balance: 1100, pendingTransactions: [\"1\"] }\n" + 240 | "]" 241 | ); 242 | 243 | transactions.insert( 244 | "{ _id: \"1\", source: \"A\", destination: \"B\", value: 100, state: #, lastModified: #}", TransactionState.PENDING, System.currentTimeMillis() - HOUR_IN_MILLISECONDS 245 | ); 246 | 247 | Transaction transaction = transactionService.findTransactionByStateAndLastModified(TransactionState.PENDING); 248 | 249 | transferService.cancelPending(transaction); 250 | 251 | Account accountA = accounts.findOne("{_id: \"A\"}").as(Account.class); 252 | assertThat(accountA.getBalance(), is(1000)); 253 | assertThat(accountA.getPendingTransactions(), is(emptyArray())); 254 | 255 | Account accountB = accounts.findOne("{_id: \"B\"}").as(Account.class); 256 | assertThat(accountB.getBalance(), is(1000)); 257 | assertThat(accountB.getPendingTransactions(), is(emptyArray())); 258 | 259 | Transaction finalTransaction = transactions.findOne().as(Transaction.class); 260 | assertThat(finalTransaction.getState(), is(TransactionState.CANCELED)); 261 | } 262 | 263 | } 264 | --------------------------------------------------------------------------------