Quarkus Panache: Choosing Between Active Record and Repository Patterns
Deciding between Panache Active Record and Repository patterns in Quarkus 3.x? Learn how these patterns impact testability, native compilation, and architecture with concrete examples.
04 Apr 2026, 02:00 UTC

The Architecture Trade-off in Data Access
When implementing data access with quarkus-hibernate-orm-panache in Quarkus 3.x, you must choose between two patterns: Active Record and Repository. While both rely on Hibernate ORM and produce identical SQL, they fundamentally change how you structure your domain logic and write your tests. The primary conflict is between development speed (Active Record) and architectural flexibility (Repository).
Active Record: Rapid Prototyping and Minimal Boilerplate
The Active Record pattern integrates data access methods directly into the entity. By extending PanacheEntity, your class gains static methods for querying and instance methods for persistence.
@Entity
public class Product extends PanacheEntity {
public String name;
public double price;
public static Product findByName(String name) {
return find("name", name).firstResult();
}
}
Key Constraint: PanacheEntity automatically provides a Long id field. If your project requires UUIDs, composite keys, or a specific ID generation strategy, you must extend PanacheEntityBase and define the @Id field manually.
Repository Pattern: Decoupling for Testability
The Repository pattern separates the domain model from the persistence logic. The entity remains a Plain Old Java Object (POJO), and data access is handled by a separate class implementing PanacheRepository.
@Entity public class Product { @Id @GeneratedValue public Long id; public String name; public double price; }@ApplicationScoped public class ProductRepository implements PanacheRepository<Product, Long> { public Product findByName(String name) { return find("name", name).firstResult(); } }
The critical advantage here is mockability. In a @QuarkusTest, you can use @InjectMock to replace the repository with a Mockito mock. Active Record relies on static methods, which are significantly harder to mock without specialized tools or starting a full database container, often turning unit tests into slower integration tests.
Practical Comparison: Testing and Execution
Consider a service that calculates a discount based on a product name. With the Repository pattern, the test is isolated:
@QuarkusTest
class DiscountServiceTest {
@InjectMock
ProductRepository productRepository;
@Inject
DiscountService discountService;
@Test
void testDiscountLogic() {
Product mockProduct = new Product();
mockProduct.price = 100.0;
when(productRepository.findByName("SaleItem")).thenReturn(mockProduct);
double result = discountService.applyDiscount("SaleItem");
assertEquals(90.0, result);
}
To verify this in a real environment, run ./mvnw quarkus:dev. Both patterns support live reload, meaning changes to your entity or repository are reflected instantly without a full restart.
Native Compilation and Virtual Threads
When targeting GraalVM native images, be aware that repository interfaces used via CDI proxies may require @RegisterForReflection to ensure the native image includes the necessary metadata. To reduce image size and startup time, set quarkus.hibernate-orm.database.generation=none for production environments.
For high-concurrency applications using Java 21+, enable virtual threads via quarkus.virtual-threads.enabled=true. You can then annotate specific repository methods with @RunOnVirtualThread. This allows the blocking JDBC call to be handled by a virtual thread, preventing the exhaustion of the platform thread pool.
Limitations and Risks
Transaction Boundaries: When using PanacheQuery.stream(), the stream must be consumed within an active transaction. If the @Transactional boundary closes before the stream is fully processed, Hibernate will throw a LazyInitializationException.
Resource Exhaustion: If you use quarkus.test.continuous-testing=true, Quarkus re-runs tests whenever an entity changes. In large projects, this can consume significant CPU. Limit the impact by setting quarkus.test.continuous-testing.limit=50 in your application.properties.
Decision Summary
| Feature | Active Record | Repository |
|---|---|---|
| Boilerplate | Very Low | Low/Medium |
| Unit Testing | Difficult (Static) | Easy (Injectable) |
| Domain Purity | Coupled to Panache | Clean POJO |
| ID Flexibility | Limited (via PanacheEntity) | Full Control |
If you are building a small internal tool or a rapid prototype, Active Record is highly efficient. For enterprise applications with complex business rules and a requirement for high test coverage, the Repository pattern is the safer engineering choice.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.