Whether you use Spring or not, you have to make a fundamental choice when you use transactions—whether to use global or local transactions. Local transactions are specific to a single transactional resource (a JDBC connection, for example), whereas global transactions are managed by the container and can span multiple transactional resources.
Local transactions are easy to manage, and because most operations work with just one transactional resource (such as a JDBC transaction), using local transactions is enough. However, if you are not using Spring, you still have a lot of transaction management code to write, and if in the future the scope of the transaction needs to be extended across multiple transactional resources, you have to drop the local transaction management code and rewrite it to use global transactions.
Global transactions in non-Spring application are, in most cases, coded using JTA, which is a complex API that depends on JNDI. This means that you have to use a J2EE application server. Rather than implementing JTA transactions programmatically, you can use EJB Container Managed Transactions (CMT) functionality, which is provided by a J2EE application server. This allows you to simply declare which operations are to be enlisted in a transaction; once you do, the container takes care of transaction management. This is the preferred way because the code you write does not contain any explicit transactional code and all the hard work is delegated to the container. Another great advantage of EJB CMT is that it removes the need to work with the JTA API directly but, by definition, you still have to use EJBs.
Ideally, you have a container that supports a global and local declarative and program- matic transactions without needing EJBs. Even though declarative transactions are the favored approach, there can be a place for programmatic transactions.