@Configuration
public class TxConfig {
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
DataSourceTransactionManager tm = new DataSourceTransactionManager(dataSource);
tm.setNestedTransactionAllowed(true);
return tm;
}
@Bean
public TransactionTemplate orderTransactionTemplate(PlatformTransactionManager tm) {
TransactionTemplate template = new TransactionTemplate(tm);
template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
template.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
template.setTimeout(10);
return template;
}
}
@Service
public class OrderCheckoutService {
private static final Logger log = LoggerFactory.getLogger(OrderCheckoutService.class);
private final TransactionTemplate template;
private final InventoryRepository inventory;
private final PaymentGateway payments;
private final LoyaltyGateway loyalty;
private final OrderRepository orders;
public OrderCheckoutService(TransactionTemplate orderTransactionTemplate,
InventoryRepository inventory,
PaymentGateway payments,
LoyaltyGateway loyalty,
OrderRepository orders) {
this.template = orderTransactionTemplate;
this.inventory = inventory;
this.payments = payments;
this.loyalty = loyalty;
this.orders = orders;
}
public Long checkout(CheckoutCommand cmd) {
return template.execute(status -> {
for (LineItem item : cmd.items()) {
int reserved = inventory.reserve(item.sku(), item.quantity());
if (reserved == 0) {
status.setRollbackOnly();
throw new OutOfStockException(item.sku());
}
}
PaymentResult payment = payments.authorize(cmd.customerId(), cmd.total());
if (!payment.approved()) {
status.setRollbackOnly();
throw new PaymentDeclinedException(payment.reason());
}
Long orderId = orders.create(cmd.customerId(), cmd.total(), payment.reference());
Object savepoint = status.createSavepoint();
try {
loyalty.accrue(cmd.customerId(), cmd.total());
status.releaseSavepoint(savepoint);
} catch (RuntimeException ex) {
// Loyalty is best-effort: undo only that step, keep the order.
status.rollbackToSavepoint(savepoint);
log.warn("Loyalty accrual skipped for order {}: {}", orderId, ex.getMessage());
}
return orderId;
});
}
}
@Repository
public class InventoryRepository {
private final JdbcTemplate jdbc;
public InventoryRepository(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
public int reserve(String sku, int quantity) {
return jdbc.update(
"UPDATE inventory SET quantity = quantity - ? " +
"WHERE sku = ? AND quantity >= ?",
quantity, sku, quantity);
}
public int availableQuantity(String sku) {
Integer qty = jdbc.queryForObject(
"SELECT quantity FROM inventory WHERE sku = ?",
Integer.class, sku);
return qty == null ? 0 : qty;
}
}
This snippet shows how a checkout flow that touches several aggregates can be composed with programmatic transaction control instead of a single @Transactional boundary. The OrderCheckoutService is the entry point: it wraps the whole flow in one TransactionTemplate so that stock reservation, payment authorization, and loyalty accrual either all commit or all roll back together.
The reason programmatic control is used here rather than the declarative annotation is that the flow has an optional step — loyalty points — that should be allowed to fail without aborting the entire order. Declarative propagation makes partial failure awkward, but a JDBC savepoint expresses it directly. In OrderCheckoutService, template.execute opens the outer transaction and TransactionStatus is used to createSavepoint() before the loyalty step. If LoyaltyGateway.accrue throws, the code calls rollbackToSavepoint(sp) and continues, so the order still commits with points skipped; on success it calls releaseSavepoint(sp).
The outer template is configured with PROPAGATION_REQUIRED and an isolation level chosen to protect the stock check. Savepoints only work inside an existing physical transaction, which is exactly what the template guarantees, and they require a DataSource that supports nested savepoints (nestedTransactionAllowed is enabled on the transaction manager in TxConfig). Setting template.setRollbackOnly() via status.setRollbackOnly() is used for the hard-failure path when payment is declined, forcing the entire outer transaction to unwind.
TxConfig wires a PlatformTransactionManager and exposes a preconfigured TransactionTemplate bean so services do not repeat isolation and timeout settings. The InventoryRepository performs the conditional UPDATE ... WHERE quantity >= ? that returns an affected-row count, letting the service detect an oversell atomically rather than reading-then-writing.
The key trade-off is added verbosity: the service now owns commit/rollback semantics explicitly. In return it gains fine-grained partial rollback that annotations cannot express, and the boundaries stay visible in one place. A common pitfall is calling savepoint APIs outside an active transaction, or expecting a savepoint to survive an outer rollback — it will not. This pattern fits multi-step workflows where some steps are best-effort while the core must remain strongly consistent.
Related snips
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
GraphQL API with Spring Boot
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
Share this code
Here's the card — post it anywhere.