A high-performance Quarkus extension for ScyllaDB/Cassandra that provides annotation-based entity mapping, automatic repository generation, and full reactive support via SmallRye Mutiny.
- Annotation-based entity mapping - Define entities with
@Table,@PartitionKey,@ClusteringKey, etc. - Automatic repository generation - Generate blocking and/or reactive repositories at build time
- Full reactive support - First-class Mutiny integration with
Uni<T>andMulti<T> - Custom queries - Define CQL queries with named parameters via
@Query - Pagination - Token-based paging optimized for ScyllaDB
- Prepared statement caching - Automatic caching for optimal performance
- Type conversion - Built-in converters with custom converter support
- CDI integration - Inject repositories directly into your beans
- Java 21+
- Quarkus 3.x
- ScyllaDB or Apache Cassandra
Add the dependency to your pom.xml:
<dependency>
<groupId>de.prgrm.quarkus-scylladb-orm</groupId>
<artifactId>quarkus-scylladb-orm</artifactId>
<version>1.0.0</version>
</dependency>import io.quarkiverse.quarkus.scylladb.orm.mapping.*;
@Table("person")
@GenerateRepository
public class Person {
@PartitionKey
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column("full_name")
private String name;
@Column
private int age;
// Getters and setters
}import jakarta.inject.Inject;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class PersonService {
@Inject
PersonBaseRepository personRepository;
public Person createPerson(String name, int age) {
Person person = new Person();
person.setName(name);
person.setAge(age);
return personRepository.save(person); // UUID auto-generated
}
public Person findById(UUID id) {
return personRepository.findById(id);
}
public List<Person> findAll() {
return personRepository.findAll();
}
}@ApplicationScoped
public class PersonReactiveService {
@Inject
PersonBaseReactiveRepository personRepository;
public Uni<Person> createPerson(String name) {
Person person = new Person();
person.setName(name);
return personRepository.save(person);
}
public Multi<Person> streamAll() {
return personRepository.findAll();
}
}| Annotation | Description |
|---|---|
@Table("name") |
Maps class to a table |
@PartitionKey |
Marks field as partition key (use ordinal for composite keys) |
@ClusteringKey |
Marks field as clustering key (supports ordinal and order) |
@Column("name") |
Maps field to column (optional, defaults to field name) |
@GeneratedValue |
Auto-generates values (UUID or SEQUENCE) |
@Transient |
Excludes field from persistence |
@Enumerated |
Enum handling (STRING or ORDINAL) |
@Convert |
Custom type conversion |
@Table("sensor_data")
@GenerateRepository
public class SensorData {
@PartitionKey(ordinal = 0)
private String sensorId;
@PartitionKey(ordinal = 1)
private String region;
@ClusteringKey(ordinal = 0, order = ClusteringOrder.DESC)
private Instant timestamp;
@Column
private double value;
}Access with composite keys:
// Find by full primary key (partition + clustering)
SensorData data = repository.findByKeys("sensor-1", "us-east", timestamp);
// Delete by full primary key
repository.deleteByKeys("sensor-1", "us-east", timestamp);// CRUD
T save(T entity)
T update(T entity)
T merge(T entity)
void delete(T entity)
void deleteById(ID id)
void deleteByKeys(Object... keys)
// Queries
T findById(ID id)
T findByKeys(Object... keys)
List<T> findAll()
long count()
// Existence
boolean exists(T entity)
boolean existsById(ID id)
// Custom CQL
List<T> query(String cql, Object... params)
T querySingle(String cql, Object... params)
void execute(String cql, Object... params)Same operations returning Mutiny types:
Uni<T>for single resultsMulti<T>for collectionsUni<Void>for void operationsUni<Long>for countsUni<Boolean>for existence checks
// First page
Pageable pageable = Pageable.ofSize(20);
Paged<Person> page1 = personRepository.findAllPaged(pageable, null);
// Process results
page1.content().forEach(this::process);
// Next page (if exists)
if (page1.hasNextPage()) {
Pageable nextPageable = Pageable.of(20, page1.nextPagingState());
Paged<Person> page2 = personRepository.findAllPaged(nextPageable, null);
}Sortable sort = Sortable.desc("created_at");
Paged<Person> page = personRepository.findAllPaged(pageable, sort);@Table("book")
@GenerateRepository
@Queries({
@Query(
name = "findByTitle",
cql = "SELECT * FROM book WHERE title = :title ALLOW FILTERING",
returnType = Query.ReturnType.SINGLE,
paramTypes = @Query.Param(name = "title", type = String.class)
),
@Query(
name = "findAllActive",
cql = "SELECT * FROM book WHERE active = :active ALLOW FILTERING",
returnType = Query.ReturnType.LIST,
paramTypes = @Query.Param(name = "active", type = Boolean.class)
),
@Query(
name = "deactivateAll",
cql = "UPDATE book SET active = false WHERE id = :id",
returnType = Query.ReturnType.VOID,
paramTypes = @Query.Param(name = "id", type = UUID.class)
)
})
public class Book {
@PartitionKey
private UUID id;
private String title;
private boolean active;
}Generated methods:
Book book = bookRepository.findByTitle("Clean Code");
List<Book> activeBooks = bookRepository.findAllActive(true);
bookRepository.deactivateAll(bookId);// Positional parameters
List<Person> results = repository.query(
"SELECT * FROM person WHERE age > ? ALLOW FILTERING",
21
);
// Named parameters
List<Person> results = repository.query(
"SELECT * FROM person WHERE name = :name AND age > :minAge ALLOW FILTERING",
Map.of("name", "John", "minAge", 21)
);Configure your ScyllaDB connection in application.properties:
# Contact points (required) - comma-separated host:port pairs
quarkus.scylla.contact-points=node1:9042,node2:9042,node3:9042
# Local datacenter (required)
quarkus.scylla.local-datacenter=datacenter1
# Default keyspace (required)
quarkus.scylla.keyspace=my_keyspace# Plain text authentication
quarkus.scylla.auth.username=cassandra
quarkus.scylla.auth.password=cassandra# Connections per local host (default: 1)
quarkus.scylla.pool.local-size=2
# Connections per remote host (default: 1)
quarkus.scylla.pool.remote-size=1
# Max requests per connection (default: 1024)
quarkus.scylla.pool.max-requests-per-connection=1024
# Heartbeat interval to keep connections alive (default: 30s)
quarkus.scylla.pool.heartbeat-interval=30s
# Connection initialization timeout (default: 5s)
quarkus.scylla.pool.connection-init-timeout=5s# Request timeout (default: 2s)
quarkus.scylla.request.timeout=2s
# Consistency level (default: LOCAL_QUORUM)
# Options: ANY, ONE, TWO, THREE, QUORUM, ALL, LOCAL_QUORUM, EACH_QUORUM, SERIAL, LOCAL_SERIAL, LOCAL_ONE
quarkus.scylla.request.consistency=LOCAL_QUORUM
# Serial consistency for LWT (default: SERIAL)
# Options: SERIAL, LOCAL_SERIAL
quarkus.scylla.request.serial-consistency=SERIAL
# Default page size for queries (default: 5000)
quarkus.scylla.request.page-size=5000# Enable SSL/TLS (default: false)
quarkus.scylla.ssl.enabled=true
# Truststore for server certificate validation
quarkus.scylla.ssl.truststore-path=/path/to/truststore.jks
quarkus.scylla.ssl.truststore-password=changeit
# Keystore for client certificate authentication (mutual TLS)
quarkus.scylla.ssl.keystore-path=/path/to/keystore.p12
quarkus.scylla.ssl.keystore-password=changeit
# Hostname verification (default: true)
quarkus.scylla.ssl.hostname-validation=true# Timeout for schema agreement after DDL statements (default: 10s)
quarkus.scylla.schema.agreement-timeout=10s
# Interval between schema agreement checks (default: 200ms)
quarkus.scylla.schema.agreement-interval=200ms
# Warn on schema agreement failure (default: true)
quarkus.scylla.schema.agreement-warn-on-failure=true# Exponential reconnection base delay (default: 1s)
quarkus.scylla.reconnection.base-delay=1s
# Exponential reconnection max delay (default: 60s)
quarkus.scylla.reconnection.max-delay=60s| Property | Description | Default |
|---|---|---|
quarkus.scylla.contact-points |
Comma-separated host:port pairs | required |
quarkus.scylla.local-datacenter |
Local datacenter name | required |
quarkus.scylla.keyspace |
Default keyspace | required |
quarkus.scylla.auth.username |
Authentication username | - |
quarkus.scylla.auth.password |
Authentication password | - |
quarkus.scylla.pool.local-size |
Connections per local host | 1 |
quarkus.scylla.pool.remote-size |
Connections per remote host | 1 |
quarkus.scylla.pool.max-requests-per-connection |
Max concurrent requests per connection | 1024 |
quarkus.scylla.pool.heartbeat-interval |
Connection heartbeat interval | 30s |
quarkus.scylla.pool.connection-init-timeout |
Connection init timeout | 5s |
quarkus.scylla.request.timeout |
Request timeout | 2s |
quarkus.scylla.request.consistency |
Default consistency level | LOCAL_QUORUM |
quarkus.scylla.request.serial-consistency |
Serial consistency for LWT | SERIAL |
quarkus.scylla.request.page-size |
Default page size | 5000 |
quarkus.scylla.ssl.enabled |
Enable SSL/TLS | false |
quarkus.scylla.ssl.truststore-path |
Path to truststore | - |
quarkus.scylla.ssl.truststore-password |
Truststore password | - |
quarkus.scylla.ssl.keystore-path |
Path to keystore (for mTLS) | - |
quarkus.scylla.ssl.keystore-password |
Keystore password | - |
quarkus.scylla.ssl.hostname-validation |
Verify server hostname | true |
quarkus.scylla.schema.agreement-timeout |
Schema agreement timeout | 10s |
quarkus.scylla.schema.agreement-interval |
Schema agreement check interval | 200ms |
quarkus.scylla.schema.agreement-warn-on-failure |
Warn on agreement failure | true |
quarkus.scylla.reconnection.base-delay |
Reconnection base delay | 1s |
quarkus.scylla.reconnection.max-delay |
Reconnection max delay | 60s |
The extension handles common Java types automatically:
UUID,String,Integer,Long,Double,Float,BooleanInstant,LocalDate,LocalDateTime,LocalTimeBigDecimal,BigIntegerbyte[],ByteBuffer- Collections:
List,Set,Map
public class JsonConverter implements AttributeConverter<MyObject, String> {
@Override
public String convertToDatabaseColumn(MyObject attribute) {
return objectMapper.writeValueAsString(attribute);
}
@Override
public MyObject convertToEntityAttribute(String dbData) {
return objectMapper.readValue(dbData, MyObject.class);
}
}
// Usage
@Table("my_table")
public class MyEntity {
@Convert(JsonConverter.class)
private MyObject data;
}Control which repositories are generated:
// Generate both (default)
@GenerateRepository
@GenerateRepository(RepositoryType.BOTH)
// Blocking only
@GenerateRepository(RepositoryType.BLOCKING)
// Reactive only
@GenerateRepository(RepositoryType.REACTIVE)The ORM automatically caches prepared statements. All repository methods use prepared statements for optimal performance.
Always use pagination when querying large datasets:
// Good
Paged<Data> page = repository.findAllPaged(Pageable.ofSize(100), null);
// Avoid for large tables
List<Data> all = repository.findAll();Design your data model with ScyllaDB's partition and clustering key model in mind:
@Table("time_series")
public class TimeSeries {
@PartitionKey
private String metricName; // Partition key for distribution
@ClusteringKey(order = ClusteringOrder.DESC)
private Instant timestamp; // Clustering for time-ordered access
private double value;
}Use reactive repositories for high-throughput scenarios:
public Multi<ProcessedData> processStream() {
return repository.findAll()
.onItem().transform(this::process)
.onFailure().retry().atMost(3);
}git clone https://github.com/UnvirtualHH/quarkus-scylladb-orm.git
cd quarkus-scylladb-orm
mvn clean installRun tests (requires Docker for Testcontainers):
mvn verifyThis project is licensed under the Apache License 2.0 - see the LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request.