Declarative Transaction Boundary using Spring @Transactional

One of the trickiest thing to do when developing database-backed application is deciding when and where to start and finish the transaction (eg: where should you draw the transaction boundary line).

Consider a very simple banking application with an AccountService class having getBalance(), debit(), credit() and transfer() methods. Transfer() calls getBalance() to validate the from-account has enough money, and then debit() the from-account and credit() the to-account.

All of the business steps of transfer() method have to be done in one transaction right? (why? — c’mon.. if you don’t do it in one transaction then it’s possible the balance got changed by another transaction between checking for sufficient money, debiting & crediting. The bank will lose reputation and have apologize to the poor customer.).

Pseudocode/sql for each method on AccountService class look somewhat like following:

  • getBalance()
    SELECT balance FROM account WHERE id = ?
  • debit() and credit()
    UPDATE account SET balance = ? WHERE id = ?
  • transfer()
                 if (getBalance(fromAccount) < amount) {
                   /*insufficient balance error */;
                 }
                 debit(fromAccount, amount);
                 credit(toAccount, amount);
               

If you programatically hard-code the transaction boundary into getBalance(), debit() and credit() method, then it might look something like this:

  public void debit(long accountId, double amount) {
    em.getTransaction().begin(); // start a new transaction
    Account account = em.find(Account.class, accountId);
    account.setBalance(account.getBalance() - amount);
    em.getTransaction().commit(); // commit
  }

But hangon, now you need to invoke all those 3 methods within 1 single transaction, your code might need to do something more complicated than that. If you change the transaction boundary into the transfer() method, then other clients that have been using getBalance(), debit(), credit() methods happily for other stuffs will be impacted — they now have to manually open and close transaction before calling those method (ugly).

Even worse, you may be considering to copy & paste the inside of getBalance(), debit() and credit() code into the transfer() method.. PLEASE DON’T!.. Remember Don’t Repeat Yourself principle!

Fortunately Spring provides a much better way of figuring out transaction boundary. All you need to do is to declare that your method needs an active transaction to be open. You do this by adding @Transactional annotation to your method, plus bootstrapping few configurations

Your AccountService methods look like following now:

  @Transactional
  public double getBalance(long accountId) {
     // do stuff here
  }

  @Transactional
  public void debit(long accountId, double amount) {
    // do stuff here
  }

  @Transactional
  public void credit(long accountId, double amount) {
    // do stuff here
  }

  @Transactional
  public void transfer(long fromAccountId, long toAccountId, double amount) {
    // call getBalance(), check from account has enough money

    // call debit()

    // call credit
  }

And the magic happens! When you call the transfer() method, Spring detects you don’t have an open transaction, so it creates a new one. When transfer() calls getBalance(), Spring won’t create a new transaction because it detects there’s a currently open one. It will assign the open transaction into getBalance() method instead. The same applies to debit() and credit(). This is also known as Transaction Propagation.

Spring does this magic via a technique called AOP (Aspect Oriented Programming). Simply speaking, Spring transaction manager will be notified everytime @Transactional annotated method is invoked / returned, and take action (start new transaction, assign existing one, close, etc.)

I’ve created a demo project with a unit test so you can try this yourself. Make sure you have jdk 1.6 and Maven 3 installed. SVN Checkout http://gerrytan.googlecode.com/svn/trunk/spring-transactional, have a feel around the classes and run mvn test. The output will be available at target/surefire-reports/com.wordpress.gerrytan.springtx.AccountServiceTest-output.txt. I’ve configured the logging such that you can see when hibernate (JPA) creates / commit / rollback transaction, and issues SQL to synchronize the persistence context.

2012-05-31 01:06:26,430 [main] DEBUG com.wordpress.gerrytan.springtx.AccountServiceTest - ----- Transfer $20 from account 2 to 1 -----
2012-05-31 01:06:26,430 [main] DEBUG org.hibernate.transaction.JDBCTransaction - begin
2012-05-31 01:06:26,431 [main] DEBUG org.hibernate.transaction.JDBCTransaction - current autocommit status: true
2012-05-31 01:06:26,431 [main] DEBUG org.hibernate.transaction.JDBCTransaction - disabling autocommit
2012-05-31 01:06:26,431 [main] DEBUG org.hibernate.SQL - select account0_.id as id0_0_, account0_.balance as balance0_0_, account0_.name as name0_0_, account0_.version as version0_0_ from account account0_ where account0_.id=?
2012-05-31 01:06:26,432 [main] DEBUG org.hibernate.SQL - select account0_.id as id0_0_, account0_.balance as balance0_0_, account0_.name as name0_0_, account0_.version as version0_0_ from account account0_ where account0_.id=?
2012-05-31 01:06:26,433 [main] DEBUG org.hibernate.transaction.JDBCTransaction - commit
2012-05-31 01:06:26,437 [main] DEBUG org.hibernate.SQL - update account set balance=?, name=?, version=? where id=? and version=?
2012-05-31 01:06:26,437 [main] DEBUG org.hibernate.SQL - update account set balance=?, name=?, version=? where id=? and version=?
2012-05-31 01:06:26,438 [main] DEBUG org.hibernate.transaction.JDBCTransaction - re-enabling autocommit
2012-05-31 01:06:26,438 [main] DEBUG org.hibernate.transaction.JDBCTransaction - committed JDBC Connection
2012-05-31 01:06:26,438 [main] DEBUG com.wordpress.gerrytan.springtx.AccountServiceTest - ----- Completed Transfer $20 from account 1 to 2 -----

Following are useful resources for further learning:

Advertisements

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s