Spring Data, JDBC, JPA and Transactions

Architect-level Notes on Spring Data and Transaction Management

Overview

Spring provides multiple data access strategies:

  1. Spring JDBC (Low-level abstraction over JDBC)

  2. Spring Data JPA (ORM-based abstraction)

  3. Transaction Management (Declarative and Programmatic)

These help achieve:

  • Clean separation of concerns

  • Reduced boilerplate

  • Consistent transaction handling

  • Scalability and maintainability


Spring Data Architecture Overview

flowchart TB
    Controller --> Service
    Service --> Repository
    Repository --> SpringData
    SpringData --> ORM[JPA Provider (Hibernate)]
    ORM --> Database[(RDBMS)]

Spring Data reduces boilerplate DAO implementation and integrates seamlessly with transaction management.


Spring JDBC

Spring JDBC is a low-level abstraction over JDBC that simplifies database access while still giving you fine-grained control over SQL and transactions. It provides the JdbcTemplate class, which handles connection management, exception handling, and reduces boilerplate code for executing SQL queries and updates. Spring JDBC is ideal for scenarios where you need high performance and fine control over SQL, such as in banking systems or when working with complex native queries. It allows you to write raw SQL while still benefiting from Spring’s features like connection pooling and transaction management. Spring provides a simple architecture for Spring JDBC, where the Service layer interacts with the JdbcTemplate, which in turn manages the DataSource and communicates with the database. This allows you to focus on your business logic while Spring handles the underlying database interactions. Spring provides data access operations performed using three main approaches:

  1. JdbcTemplate: A central class that simplifies JDBC operations and error handling. It provides methods for executing SQL queries, updates, and stored procedures, as well as for mapping result sets to Java objects. It also supports batch operations and transaction management. JdbcTemplate internally manages database connections, handles exceptions, and reduces the boilerplate code typically associated with JDBC. It allows developers to focus on writing SQL queries and business logic rather than dealing with low-level JDBC details. It implements the Template Method design pattern, where it provides a template for executing database operations while allowing developers to customize the behavior through callback interfaces.

  2. NamedParameterJdbcTemplate: An extension of JdbcTemplate that allows the use of named parameters in SQL queries for better readability. It provides methods for executing SQL queries with named parameters, which can improve code clarity and maintainability. Named parameters are specified using a colon followed by the parameter name (e.g., :name) in the SQL query, and the values are provided as a Map or a SqlParameterSource object. This approach can make complex queries easier to read and understand compared to using positional parameters.

  3. SimpleJdbcInsert and SimpleJdbcCall: Helper classes for simplifying insert and stored procedure calls, respectively. SimpleJdbcInsert provides a convenient way to perform insert operations without having to write SQL statements, while SimpleJdbcCall simplifies the execution of stored procedures by handling the necessary metadata and parameter mapping. These classes can further reduce boilerplate code and improve the readability of database operations in Spring applications.

Spring JDBC simplifies JDBC by:

  • Managing connections

  • Handling exceptions

  • Reducing try-catch-finally boilerplate

JdbcTemplate Architecture

Diagram

Example: JdbcTemplate Configuration

@Configuration
public class JdbcConfig {

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

Example: Repository Using JdbcTemplate

@Repository
public class UserJdbcRepository {

    private final JdbcTemplate jdbcTemplate;

    public UserJdbcRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public User findById(Long id) {
        return jdbcTemplate.queryForObject(
            "SELECT * FROM users WHERE id = ?",
            new BeanPropertyRowMapper<>(User.class),
            id
        );
    }
}

When to Use Spring JDBC:

  • High-performance systems

  • Complex native queries

  • Banking systems with fine SQL control


JPA (Java Persistence API)

JPA is an ORM specification. Hibernate is the most common implementation. JPA abstracts away SQL and allows you to work with Java objects (entities) instead of database tables. JPA provides features like: - Object-relational mapping - Lazy loading - Caching - Transaction management JPA is ideal for applications where you want to focus on business logic rather than SQL, such as in typical web applications. It can improve developer productivity and maintainability by reducing boilerplate code and providing a more object-oriented approach to data access. However, it may not be suitable for performance-critical applications or when you need fine control over SQL, as it can introduce overhead and may not always generate optimal queries. ORMs like JPA are great for rapid development and maintainability, but they can introduce performance overhead and may not always generate optimal SQL queries. In scenarios where performance is critical or when you need fine control over SQL, such as in banking systems or when working with complex native queries, using Spring JDBC might be a better choice. JPA abstracts away the underlying database interactions, which can lead to inefficiencies in certain cases, while Spring JDBC allows you to write raw SQL and optimize it as needed.

JPA provides annotations for mapping Java classes to database tables, such as @Entity, @Table, @Id, and @GeneratedValue. It also supports relationships between entities using annotations like @OneToMany, @ManyToOne, and @ManyToMany. JPA uses a persistence context to manage the lifecycle of entities and provides features like dirty checking and lazy loading to optimize database interactions. It also integrates with Spring’s transaction management to ensure data integrity and consistency.

JPA Architecture

Diagram

Key Concepts:

  • Entity

  • Persistence Context

  • EntityManager

  • Dirty Checking

  • Lazy vs Eager loading

Example: Entity

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
}

Example: EntityManager Usage

@Service
public class UserService {

    @PersistenceContext
    private EntityManager entityManager;

    public User find(Long id) {
        return entityManager.find(User.class, id);
    }
}

Spring Data JPA

Spring Data JPA builds on top of JPA and removes DAO boilerplate. It provides a repository abstraction that allows you to define repository interfaces and Spring will auto-implement them at runtime. This means you can focus on defining your domain model and repository interfaces without having to write the implementation code for common CRUD operations. Spring Data JPA also provides powerful features like derived query methods, pagination and sorting, auditing support, specification support, and the ability to define custom queries using the @Query annotation. It is ideal for applications where you want to rapidly develop data access layers with minimal boilerplate code while still benefiting from the features of JPA. However, it may not be suitable for performance-critical applications or when you need fine control over SQL, as it abstracts away the underlying database interactions and may not always generate optimal queries. In such cases, using Spring JDBC or plain JPA might be a better choice.

Three Options for JPA Setup in a Spring Environment: Spring offers three different options to configure EntityManagerFactory in a project:

  • LocalEntityManagerFactoryBean: This is the simplest option and is typically used for testing or standalone applications. It reads the JPA configuration from the persistence.xml file located in the classpath. It does not allow you to use a Spring-managed DataSource instance and does not support distributed transaction management.

@Configuration
public class Ch5Configuration {
    @Bean
    public LocalEntityManagerFactoryBean entityManagerFactory() {
        LocalEntityManagerFactoryBean factoryBean =
            new LocalEntityManagerFactoryBean();
        factoryBean.setPersistenceUnitName("test-jpa");
        return factoryBean;
    }
}
  • EntityManagerFactory lookup over JNDI: This option allows you to look up an EntityManagerFactory that has been configured and registered in a JNDI registry. This is commonly used in Java EE environments where the application server manages the EntityManagerFactory and provides it via JNDI.

@Configuration
public class Ch5Configuration {
    @Bean
    public JndiObjectFactoryBean entityManagerFactory() {
        JndiObjectFactoryBean factoryBean = new JndiObjectFactoryBean();
        factoryBean.setJndiName("persistence/test-jpa");
        return factoryBean;
    }
}
  • LocalContainerEntityManagerFactoryBean: This is the most flexible and commonly used option in Spring applications. It allows you to configure the EntityManagerFactory using Spring’s configuration mechanisms, such as Java-based configuration or XML. It also allows you to use a Spring-managed DataSource instance and supports distributed transaction management. This option is recommended for most applications as it provides the most control and integration with Spring’s features.

@Configuration
public class Ch5Configuration {
@Bean
    public DataSource dataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("org.h2.Driver");
        dataSource.setUrl("jdbc:h2:tcp://localhost/~/test");
        dataSource.setUsername("sa");
        dataSource.setPassword("");
        return dataSource;
    }
}

Repository Abstraction

@Repository
public interface UserRepository
        extends JpaRepository<User, Long> {

    List<User> findByName(String name);
}

Spring auto-implements this at runtime.

Internal Flow

Diagram

Benefits:

  • Derived query methods

  • Pagination & Sorting

  • Auditing support

  • Specification support

  • Custom query with @Query


Transactions in Spring

Transactions are a fundamental concept in software development that ensure data integrity and consistency. They allow you to group multiple operations into a single unit of work that either succeeds or fails as a whole. In Spring, transactions can be managed declaratively using the @Transactional annotation or programmatically using the TransactionTemplate class. Spring’s transaction management supports various propagation behaviors and isolation levels, allowing you to control how transactions interact with each other and with the underlying database. Proper transaction management is crucial for ensuring data integrity, especially in applications that involve multiple database operations or interactions with external systems.

TranasactionAwareDataSourceProxy is a Spring class that provides a proxy for a DataSource, allowing it to be aware of Spring-managed transactions. It ensures that the same database connection is used throughout a transaction, even if multiple components access the DataSource. This is particularly important when using Spring’s declarative transaction management with @Transactional, as it allows for proper transaction propagation and ensures that all operations within a transaction are executed on the same connection. By using TransactionAwareDataSourceProxy, you can avoid issues related to connection management and ensure that your transactions are properly coordinated across different components of your application.

Transactions ensure:

  • Atomicity: All operations within a transaction either succeed or fail together. Also known as "all-or-nothing" principle. If any operation within the transaction fails, the entire transaction is rolled back to maintain data integrity. This is crucial for ensuring that the database remains in a consistent state, even in the face of errors or exceptions. Atomicity is also known as unit of work.

  • Consistency: Transactions must transition the database from one consistent state to another. This means that any transaction must adhere to all defined rules, constraints, and triggers in the database. If a transaction violates any of these rules, it will be rolled back to maintain the integrity of the data. Consistency ensures that the database remains in a valid state before and after the transaction is executed.

  • Isolation: Transactions should not interfere with each other. The changes made by one transaction should not be visible to other transactions until the first transaction is committed. This prevents issues such as dirty reads, non-repeatable reads, and phantom reads. Isolation levels can be configured to control the degree of visibility between transactions. Higher isolation levels provide stronger guarantees but can lead to increased contention and reduced concurrency.

  • Durability (ACID): Once a transaction is committed, its changes are permanent and will survive any subsequent system failures. This means that even if the database crashes immediately after a transaction is committed, the changes made by that transaction will not be lost. Durability is typically achieved through the use of transaction logs and other mechanisms that ensure that committed transactions are safely stored and can be recovered in the event of a failure. Durability is the "D" in the ACID properties of transactions, which stands for Atomicity, Consistency, Isolation, and Durability. These properties are essential for ensuring the reliability and integrity of database transactions.

Spring Transaction Management:

Each data access technology has its own transaction mechanism. In other words, they provide different APIs to begin a new transaction, commit the transaction when data operations finish with success, or roll it back in case an error occurs. This is called transaction demarcation.

Spring’s transaction abstraction model is based on the PlatformTransactionManager interface. Different concrete implementations of it exist, and each one corresponds to one particular data access technology. As a developer, your responsibility is to decide which PlatformTransactionManager implementation will be used by Spring Container. This decision results in a bean definition called transactionManager, by default.

Local Versus Global Transactions:

Spring supports both local and global transactions. Local transactions are specific to a single resource, such as a database, and are managed by the resource itself. Global transactions, on the other hand, can span multiple resources and are managed by a transaction manager that coordinates the transaction across all involved resources. Spring’s transaction management abstraction allows you to work with both types of transactions seamlessly, providing a consistent programming model regardless of the underlying transaction management mechanism. This flexibility is particularly useful in complex applications that may need to interact with multiple databases or other transactional resources.

Advantages of Spring’s Abstract Transaction Model: 1. Consistency: It provides a consistent programming model for transaction management across different data access technologies, allowing developers to work with transactions in a uniform way regardless of the underlying implementation. 2. Flexibility: It supports both local and global transactions, giving developers the ability to choose the appropriate transaction management strategy for their application. 3. Integration: It integrates seamlessly with Spring’s declarative transaction management using the @Transactional annotation, allowing developers to manage transactions declaratively without having to write boilerplate code for transaction management. 4. Support for Multiple Transaction Managers: It allows you to use different transaction managers for different data access technologies within the same application, providing flexibility in how transactions are managed across various components of the application. 5. Exception Translation: It provides a consistent exception hierarchy for transaction-related exceptions, allowing developers to handle transaction exceptions in a consistent way regardless of the underlying transaction management mechanism

Spring provides two main approaches to transaction management:

  1. Declarative Transactions: This is the most common approach and is typically used in Spring applications. It allows you to manage transactions declaratively using the @Transactional annotation. You can apply this annotation to methods or classes, and Spring will automatically handle the transaction management for you. This approach is recommended for most applications as it provides a clean separation of concerns and allows you to focus on your business logic rather than transaction management.

  2. Programmatic Transactions: This approach allows you to manage transactions programmatically using the TransactionTemplate class. It provides more fine-grained control over transaction boundaries and is typically used in scenarios where you need to manage transactions in a more complex way, such as when you need to handle multiple transactions within a single method or when you need to manage transactions across multiple threads. However, it can lead to more verbose code and is generally not recommended for most applications unless you have specific requirements that cannot be met with declarative transactions.

Spring supports:

  1. Declarative Transactions (@Transactional)

  2. Programmatic Transactions


Spring uses AOP proxies.

Diagram

Example

@Service
public class TransferService {

    @Transactional
    public void transfer(Account from, Account to, BigDecimal amount) {
        from.debit(amount);
        to.credit(amount);
    }
}

Default Transactional Behavior:

The following are the default values of @Transactional annotation:

  • propagation: REQUIRED

  • isolation: DEFAULT

  • timeout: TIMEOUT_DEFAULT

  • readOnly: false

  • rollbackFor: java.lang.RuntimeException or its subclasses

  • noRollbackFor: java.lang.Exception or its subclasses

The propagation attribute defines the scope of the transaction, whether it spans multiple method invocations, and so on. Its values can be REQUIRED, REQUIRES_NEW, NESTED, SUPPORTS, NOT_SUPPORTED, MANDATORY, or NEVER. More information about how propagation works is given in the next section.

The isolation attribute specifies the underlying database system’s isolation level. Possible values are READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, and SERIALIZABLE. However, you must be aware that the underlying database system should have support for the given value to be active.

The timeout value specifies the transaction timeout period. It is directly passed into the underlying database system.

The readOnly attribute is actually a hint to the underlying transaction subsystem, and it tells the transaction subsystem that the method performs only read operations but the transaction should still be active. If the underlying subsystem doesn’t understand, it causes no harm to the current transaction, and changes reflect to the database.

The rollbackFor and noRollbackFor attributes expect classes, and they specify what happens when an exception occurs while executing the transactional method.

If you need to customize any of these attributes, just add the corresponding attribute in your @Transactional annotation.

@Transactional
public class AccountServiceImpl implements AccountService {

    @Override
    public void transferMoney(
        long sourceAccountId, long targetAccountId, double amount) {
        //...
    }

    @Override
    @Transactional(rollbackFor=Exception.class)
    public void depositMoney(long accountId, double amount) throws Exception {
        //...
    }

    @Override
    @Transactional(readOnly=true)
    public Account getAccount(long accountId) {
        //...
    }
}

IMPORTANT:It is good practice to put @Transactional annotations on a class and on its public methods. It is possible to place an @Transactional annotation on an interface and on its methods, too. However, doing so is discouraged because of Spring’s proxy generation mechanism. Spring has interface-based and class-based proxy generation mechanisms. If class-based proxy generation is used, the generated proxy class inherits from your bean’s class, and that proxy won’t inherit annotations from its interface. So, if you put @Transactional on an interface and use class-based proxy generation, the transactional behavior won’t be applied to your bean. To avoid this issue, it’s recommended to place @Transactional annotations on the class and its public methods, ensuring that the transactional behavior is correctly applied regardless of the proxy generation mechanism used by Spring.

Key Points:

  • Default propagation: REQUIRED

  • Rollback on RuntimeException

  • Configurable isolation levels


Transaction Propagation Types

| Propagation | Description | |------------|--------------| | REQUIRED | Join existing or create new | | REQUIRES_NEW | Always create new | | SUPPORTS | Use if exists | | NOT_SUPPORTED | Run without transaction | | MANDATORY | Must have transaction | | NEVER | Must not have transaction | | NESTED | Nested transaction |

Example:

@Transactional(propagation = Propagation.REQUIRES_NEW)

Isolation Levels

| Isolation | Prevents | |-----------|----------| | READ_UNCOMMITTED | Nothing | | READ_COMMITTED | Dirty reads | | REPEATABLE_READ | Non-repeatable reads | | SERIALIZABLE | Phantom reads |

Example:

@Transactional(isolation = Isolation.SERIALIZABLE)

Banking systems often use: - READ_COMMITTED or REPEATABLE_READ


Programmatic Transactions

Using TransactionTemplate:

@Service
public class PaymentService {

    private final TransactionTemplate template;

    public PaymentService(PlatformTransactionManager txManager) {
        this.template = new TransactionTemplate(txManager);
    }

    public void process() {
        template.execute(status -> {
            // business logic
            return null;
        });
    }
}

Used when:

  • Fine-grained control needed

  • Complex transaction flows


In most cases, declarative transactions with @Transactional are sufficient and recommended for their simplicity and maintainability. Programmatic transactions should be used only when you have specific requirements that cannot be met with declarative transactions, as they can lead to more verbose and less maintainable code.

Banking Architecture Perspective

Banking Systems:

  • Strong ACID

  • Strict isolation

  • Audit logging

  • Optimistic/Pessimistic locking

Example:

@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<Account> findById(Long id);

Recommended:

  • Spring MVC

  • Spring Data JPA

  • Strong transaction boundaries

  • RDBMS (Oracle/Postgres)


FAANG-Scale Perspective

FAANG Systems:

  • Distributed systems

  • Event-driven

  • CQRS

  • Eventual consistency

  • Polyglot persistence

Architecture:

Diagram

Patterns:

  • Saga pattern

  • Outbox pattern

  • CQRS

  • Event sourcing

Reactive + R2DBC may be used in high concurrency systems.


Best Practices (Architect Level)

  1. Keep transactions in Service layer

  2. Never call transactional method internally (self-invocation issue)

  3. Keep transactions short

  4. Avoid long-running remote calls inside transactions

  5. Use read-only transactions when applicable

Example:

@Transactional(readOnly = true)
public List<User> findAll() { }

Final Comparison

Spring offers different options for data access, and the choice between Spring JDBC, JPA, and Spring Data JPA depends on the specific requirements of your application. Spring JDBC provides fine-grained control over SQL and is suitable for performance-critical applications or when working with complex native queries. JPA abstracts away SQL and allows you to work with Java objects, making it ideal for typical web applications where developer productivity and maintainability are priorities. Spring Data JPA builds on top of JPA and removes boilerplate code, allowing for rapid development of data access layers while still benefiting from JPA’s features. However, it may not be suitable for performance-critical applications or when you need fine control over SQL, as it abstracts away the underlying database interactions.

| Feature | JDBC | JPA | Spring Data JPA | |----------|------|------|----------------| | Abstraction | Low | Medium | High | | Control | High | Medium | Medium | | Boilerplate | High | Medium | Low | | Performance tuning | Manual | Good | Good | | Learning Curve | Low | Medium | Low |


Interview Summary

Spring JDBC → Fine control, performance tuning JPA → ORM abstraction Spring Data JPA → Rapid development @Transactional → Declarative ACID management Banking → Strong consistency FAANG → Distributed + Event-driven