This episode covers transaction management with Narayana/JTA, propagation behavior and rollback, JPA optimization with fetch strategies and caching, as well as database migrations with Flyway or Liquibase.

In episode 6 you stored data with Panache. But a production application needs more than simple CRUD: multi-table operations that must succeed or fail together, queries that aren't wasteful, and database schemas that evolve safely.
Episode 10 covers the advanced persistence layer: transaction management with Narayana/JTA, propagation behavior and rollback, JPA optimization with fetch strategies and caching, as well as database migrations with Flyway or Liquibase.
Quarkus uses Narayana as its JTA implementation. Transactions are started and ended automatically with the @Transactional annotation:
import jakarta.transaction.Transactional;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped
public class OrderService {
@Inject
OrderRepository orderRepository;
@Inject
StockRepository stockRepository;
@Transactional
public void buatOrder(Order order) {
orderRepository.persist(order);
stockRepository.kurangiStok(order.itemId, order.qty);
}
}If kurangiStok throws an exception, the whole transaction is rolled back — the order isn't saved and the stock isn't reduced. @Transactional guarantees atomicity.
A runtime exception triggers automatic rollback, while a checked exception does not. For checked exceptions, set rollbackOn:
import jakarta.transaction.Transactional;
@Transactional(rollbackOn = CustomBusinessException.class)
public void buatOrder() throws CustomBusinessException {
// if CustomBusinessException is thrown, the transaction is rolled back
}Or use dontRollbackOn to exclude certain exceptions from rollback.
JTA in Quarkus supports propagation modes on @Transactional:
REQUIRED: join an existing transaction or create a new one. This is the default.REQUIRES_NEW: always start a new transaction, pausing the outer one.MANDATORY: a transaction must already exist, otherwise it errors.NEVER: refuses if a transaction already exists.SUPPORTS: joins a transaction if one exists, runs without one otherwise.For example, @Transactional(Transactional.TxType.REQUIRES_NEW) is useful for writing audit logs that stay saved even if the main operation fails.
You can trigger a rollback explicitly with TransactionManager: in a catch block, call tm.setRollbackOnly() then rethrow the exception. The commit at the end of the transaction automatically becomes a rollback.
JPA relationships have lazy and eager modes. The default for @OneToMany is lazy — child data is only loaded when accessed. To avoid N+1 queries, use a fetch join with PanacheQL:
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class OrderRepository implements PanacheRepository<Order> {
public Order cariDenganItems(Long id) {
return find("select o from Order o "
+ "left join fetch o.items where o.id = ?1", id)
.firstResult();
}
}left join fetch o.items fetches the order along with its items in a single query — avoiding hundreds of small queries.
Hibernate supports a second-level cache for entities that rarely change. Activate it with the following configuration:
Activate it with quarkus.hibernate-orm.cache.enabled=true. For very static data, cache at the application level with @CacheResult(cacheName = "produk") — repeated calls with the same id never touch the database.
drop-and-create is only safe in development. In production, schema changes must be controlled. Flyway and Liquibase manage database schema versions.
Add it with ./mvnw quarkus:add-extension -Dextensions=flyway, then configure:
quarkus.flyway.migrate-at-start=true
quarkus.flyway.locations=db/migrationCreate the migration file in src/main/resources/db/migration:
CREATE TABLE orders (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
item_id BIGINT NOT NULL,
qty INT NOT NULL
);The file name follows the pattern V<n>__<description>.sql. When the application starts, Flyway runs the migrations that haven't been executed yet. The command ./mvnw quarkus:add-extension -Dextensions=flyway adds the extension.
If you choose Liquibase, its extension is:
If you choose Liquibase, add it with ./mvnw quarkus:add-extension -Dextensions=liquibase. Both are solid. Pick one and stay consistent. Important principle: migrations can only be added, never modified after they've been executed in an existing environment.
Episode 10 strengthens your application's data layer: understanding Narayana transactions with @Transactional, propagation and rollback control, JPA optimization with fetch strategies and the second-level cache, as well as safe database schema management with Flyway or Liquibase.
Key takeaways:
@Transactional makes operations atomic with automatic rollback on runtime exceptions.rollbackOn and dontRollbackOn control exception behavior.REQUIRES_NEW is suitable for audit logs that commit separately.In episode 11 we'll cover batch jobs and scheduling — Quarkus Scheduler for scheduled tasks, building batch processing, task executor and concurrency control configuration, as well as job monitoring and retry handling.