Back to Blog

Quarkus Reactive Deep Dive: Data Layer's Reactive Architecture

1. [Quarkus in coomia-dip Data Layer](#1-quarkus-in-coomia-dip-data-Layer)

CoomiaPublished on November 23, 20259 min read
Share this articleTwitter / X

Series: S8 Technology Deep Dives · Article 14 | Level: Advanced | Reading Time: 20 min

Quarkus Reactive Deep Dive: Data Layer's Reactive Architecture

#TL;DR

  • Quarkus 3.x is the core framework for coomia-dip's Data Layer (Data Layer), leveraging reactive programming to achieve high-throughput, low-latency data services
  • This article deeply analyzes Quarkus's Vert.x event loop model, Mutiny reactive API, RESTEasy Reactive, Hibernate Reactive with Panache, and GraalVM Native Image compilation
  • Covers Quarkus + gRPC integration, reactive database access, backpressure handling, and coomia-dip performance benchmarks

#Table of Contents

  1. Quarkus in coomia-dip Data Layer
  2. Vert.x Event Loop Model
  3. Mutiny Reactive API
  4. RESTEasy Reactive
  5. Hibernate Reactive and Panache
  6. gRPC Service Integration
  7. Reactive Messaging
  8. Backpressure and Flow Control
  9. GraalVM Native Image
  10. Performance Benchmarks and Tuning
  11. Key Takeaways

#1. Quarkus in coomia-dip Data Layer

#1.1 Why Quarkus?

DimensionSpring BootQuarkus
Startup time3-10s0.5-2s (JVM) / 0.02s (Native)
Memory footprint200-500 MB50-150 MB (JVM) / 20-50 MB (Native)
Reactive supportWebFlux (optional)Native (Vert.x core)
Compile-time optimizationLimitedExtensive (ArC CDI, build-time augmentation)
GraalVM compatibilityRequires heavy configFirst-class citizen
Use caseControl Layer (Control Layer)Data Layer (Data Layer)

#1.2 Data Layer Service Matrix

Code
Data Layer Services (Quarkus 3.x):
│
├── QueryService          — OQL query execution
├── SearchService         — Full-text search
├── AnalyticsQueryService — Aggregation analytics
├── SubscriptionService   — Real-time subscription push
├── PipelineService       — Data pipeline management
├── MaterializationService — Materialized views
└── StorageService        — Iceberg/Doris storage abstraction

#2. Vert.x Event Loop Model

#2.1 Event Loop Architecture

Code
┌─────────────────────────────────────────────┐
│              Quarkus Application            │
│                                              │
│  ┌──────────────────────────────────────┐   │
│  │          IO Thread Pool              │   │
│  │  (Event Loop Threads = CPU cores)    │   │
│  │                                      │   │
│  │  Thread-0 ─── Event Loop ──→ Handle  │   │
│  │  Thread-1 ─── Event Loop ──→ Handle  │   │
│  │  Thread-N ─── Event Loop ──→ Handle  │   │
│  └──────────────────────────────────────┘   │
│                                              │
│  ┌──────────────────────────────────────┐   │
│  │         Worker Thread Pool           │   │
│  │   (for blocking operations)          │   │
│  │                                      │   │
│  │  Worker-0 ─── Blocking Task          │   │
│  │  Worker-1 ─── Blocking Task          │   │
│  └──────────────────────────────────────┘   │
└─────────────────────────────────────────────┘

#2.2 The Golden Rule of Event Loops

Java
// ❌ NEVER execute blocking operations on event loop threads
@Path("/query")
public class QueryResource {

    @GET
    public Uni<QueryResult> query(@QueryParam("oql") String oql) {
        // ❌ Blocking call freezes the event loop
        // return Uni.createFrom().item(blockingDbQuery(oql));

        // ✅ Use reactive client
        return reactiveClient.execute(oql)
            .map(rows -> QueryResult.from(rows));
    }
}

// ✅ If blocking is necessary, annotate with @Blocking
@Path("/legacy")
public class LegacyResource {

    @GET
    @Blocking  // Automatically dispatched to Worker thread pool
    public QueryResult legacyQuery(@QueryParam("sql") String sql) {
        return jdbcClient.query(sql);  // Blocking JDBC call
    }
}

#2.3 Thread Model Configuration

PROPERTIES
# application.properties

# IO thread count (default = CPU cores)
quarkus.vertx.event-loops-pool-size=8

# Worker thread pool size
quarkus.vertx.worker-pool-size=20

# Max event loop execution time (timeout warning)
quarkus.vertx.max-event-loop-execute-time=2s
quarkus.vertx.warning-exception-time=2s

#3. Mutiny Reactive API

#3.1 Uni and Multi

Java
// Uni<T> — Represents an async operation yielding 0 or 1 elements
Uni<OntologyInstance> uni = instanceRepository
    .findById(instanceId);

// Multi<T> — Represents an async stream of 0 to N elements
Multi<OntologyInstance> multi = instanceRepository
    .findByType(objectType);

#3.2 Operator Composition

Java
@ApplicationScoped
public class OntologyQueryService {

    @Inject
    ReactiveOntologyRepository repository;

    @Inject
    ReactiveCacheService cache;

    public Uni<QueryResult> executeQuery(OqlQuery query) {
        return parseQuery(query)
            .chain(parsed -> {
                // Check cache first
                return cache.get(parsed.cacheKey())
                    .onItem().ifNull().switchTo(() ->
                        // Cache miss, query database
                        repository.execute(parsed)
                            .chain(result ->
                                // Write result to cache
                                cache.put(parsed.cacheKey(), result)
                                    .replaceWith(result)
                            )
                    );
            })
            .onFailure().retry()
                .withBackOff(Duration.ofMillis(100), Duration.ofSeconds(1))
                .atMost(3)
            .onFailure().recoverWithItem(error -> {
                log.error("Query failed", error);
                return QueryResult.error(error.getMessage());
            });
    }

    public Multi<OntologyInstance> streamInstances(String objectType) {
        return repository.findByType(objectType)
            .select().where(instance -> instance.isActive())
            .onItem().transform(instance -> enrichWithDerivedProperties(instance))
            .group().intoLists().of(100)  // Batch every 100 items
            .onItem().transformToUniAndMerge(batch ->
                processBatch(batch)
            );
    }
}

#3.3 Concurrent Operations

Java
// Execute multiple async operations in parallel
public Uni<EnrichedInstance> enrichInstance(String instanceId) {
    Uni<OntologyInstance> instanceUni = repository.findById(instanceId);
    Uni<List<LinkType>> linksUni = linkRepository.findBySourceId(instanceId);
    Uni<Map<String, Object>> metricsUni = metricService.getMetrics(instanceId);

    return Uni.combine().all()
        .unis(instanceUni, linksUni, metricsUni)
        .with((instance, links, metrics) ->
            new EnrichedInstance(instance, links, metrics)
        );
}

#4. RESTEasy Reactive

#4.1 External REST API

Java
@Path("/api/v1/ontology")
@Produces(MediaType.APPLICATION_JSON)
@ApplicationScoped
public class OntologyResource {

    @Inject
    OntologyQueryService queryService;

    @GET
    @Path("/instances/{objectType}")
    public Multi<OntologyInstance> listInstances(
            @PathParam("objectType") String objectType,
            @QueryParam("limit") @DefaultValue("100") int limit,
            @QueryParam("offset") @DefaultValue("0") int offset) {
        return queryService.findByType(objectType)
            .skip().first(offset)
            .select().first(limit);
    }

    @POST
    @Path("/query")
    @Consumes(MediaType.APPLICATION_JSON)
    public Uni<QueryResult> executeQuery(OqlQueryRequest request) {
        return queryService.executeQuery(request.toOqlQuery());
    }

    @GET
    @Path("/instances/{objectType}/{instanceId}")
    @RestStreamElementType(MediaType.APPLICATION_JSON)
    public Multi<ServerSentEvent<OntologyEvent>> streamEvents(
            @PathParam("objectType") String objectType,
            @PathParam("instanceId") String instanceId) {
        // Server-Sent Events stream
        return subscriptionService.subscribe(objectType, instanceId)
            .map(event -> Sse.event(event).id(event.getId()));
    }
}

#4.2 Request Filters

Java
@Provider
@Priority(Priorities.AUTHENTICATION)
public class WorldContextFilter implements ContainerRequestFilter {

    @Override
    public void filter(ContainerRequestContext requestContext) {
        String worldId = requestContext.getHeaderString("X-World-Id");
        if (worldId == null || worldId.isBlank()) {
            requestContext.abortWith(
                Response.status(400)
                    .entity(Map.of("error", "X-World-Id header required"))
                    .build()
            );
            return;
        }
        WorldContext.setCurrent(worldId);
    }
}

#5. Hibernate Reactive and Panache

#5.1 Reactive Repository

Java
@ApplicationScoped
public class OntologyInstanceRepository
        implements PanacheRepositoryBase<OntologyInstanceEntity, String> {

    public Uni<OntologyInstanceEntity> findByObjectId(String objectId) {
        return find("objectId", objectId).firstResult();
    }

    public Multi<OntologyInstanceEntity> findByType(String objectType) {
        return find("objectType", objectType).stream();
    }

    public Uni<Long> countByType(String objectType) {
        return count("objectType", objectType);
    }

    public Uni<List<OntologyInstanceEntity>> search(
            String objectType,
            Map<String, Object> filters,
            int limit,
            int offset) {
        StringBuilder query = new StringBuilder("objectType = :type");
        Parameters params = Parameters.with("type", objectType);

        for (Map.Entry<String, Object> filter : filters.entrySet()) {
            query.append(" AND properties->>'")
                 .append(filter.getKey())
                 .append("' = :")
                 .append(filter.getKey());
            params.and(filter.getKey(), filter.getValue().toString());
        }

        return find(query.toString(), params)
            .page(Page.of(offset / limit, limit))
            .list();
    }
}

#5.2 Entity Definition

Java
@Entity
@Table(name = "ontology_instances")
public class OntologyInstanceEntity extends PanacheEntityBase {

    @Id
    public String id;

    @Column(name = "object_type", nullable = false)
    public String objectType;

    @Column(name = "object_id", nullable = false, unique = true)
    public String objectId;

    @Column(name = "world_id", nullable = false)
    public String worldId;

    @Type(JsonBinaryType.class)
    @Column(name = "properties", columnDefinition = "jsonb")
    public Map<String, Object> properties;

    @Column(name = "version")
    @Version
    public long version;

    @CreationTimestamp
    @Column(name = "created_at")
    public Instant createdAt;

    @UpdateTimestamp
    @Column(name = "updated_at")
    public Instant updatedAt;
}

#6. gRPC Service Integration

#6.1 gRPC Service Definition

Java
@GrpcService
public class QueryGrpcService extends MutinyQueryServiceGrpc.QueryServiceImplBase {

    @Inject
    OntologyQueryService queryService;

    @Override
    public Uni<QueryResponse> executeQuery(QueryRequest request) {
        return queryService.executeQuery(toOqlQuery(request))
            .map(result -> QueryResponse.newBuilder()
                .addAllInstances(result.getInstances().stream()
                    .map(this::toProto)
                    .toList())
                .setTotalCount(result.getTotalCount())
                .build()
            );
    }

    @Override
    public Multi<InstanceEvent> streamChanges(StreamRequest request) {
        return queryService.streamChanges(
            request.getObjectType(),
            request.getFilterExpression()
        ).map(this::toProtoEvent);
    }
}

#6.2 gRPC Client

Java
@ApplicationScoped
public class ControlPlaneClient {

    @GrpcClient("control-Layer")
    MutinyOntologyServiceGrpc.MutinyOntologyServiceStub ontologyStub;

    public Uni<OntologySchema> getSchema(String objectType) {
        return ontologyStub.getSchema(
            GetSchemaRequest.newBuilder()
                .setObjectType(objectType)
                .build()
        ).map(this::fromProto);
    }
}
PROPERTIES
# gRPC client configuration
quarkus.grpc.clients.control-Layer.host=control-Layer-service
quarkus.grpc.clients.control-Layer.port=9090
quarkus.grpc.clients.control-Layer.plain-text=true

#7. Reactive Messaging

#7.1 Kafka Reactive Integration

Java
@ApplicationScoped
public class CdcEventProcessor {

    @Incoming("cdc-events")
    @Outgoing("processed-events")
    public Multi<Record<String, ProcessedEvent>> process(
            Multi<Record<String, CdcEvent>> events) {
        return events
            .onItem().transformToUniAndMerge(record -> {
                CdcEvent event = record.value();
                return enrichEvent(event)
                    .map(enriched -> Record.of(
                        record.key(),
                        new ProcessedEvent(enriched)
                    ));
            })
            .select().where(record -> record.value().isValid());
    }
}
PROPERTIES
# Kafka connector configuration
mp.messaging.incoming.cdc-events.connector=smallrye-kafka
mp.messaging.incoming.cdc-events.topic=coomia-dip.cdc.events
mp.messaging.incoming.cdc-events.value.deserializer=io.quarkus.kafka.client.serialization.JsonbDeserializer
mp.messaging.incoming.cdc-events.group.id=data-Layer-processor
mp.messaging.incoming.cdc-events.auto.offset.reset=latest
mp.messaging.incoming.cdc-events.failure-strategy=dead-letter-queue

#8. Backpressure and Flow Control

#8.1 Mutiny Backpressure Handling

Java
public Multi<OntologyInstance> streamWithBackpressure(String objectType) {
    return repository.findByType(objectType)
        // Control downstream consumption rate
        .onOverflow()
            .buffer(1000)        // Buffer 1000 items
            .drop()              // Drop new elements when buffer full
        // Or use pacing strategy
        .paceDemand()
            .using(1, Duration.ofMillis(10));  // Request 1 item every 10ms
}

#8.2 gRPC Stream Backpressure

Java
@GrpcService
public class StreamingGrpcService
        extends MutinyStreamServiceGrpc.StreamServiceImplBase {

    @Override
    public Multi<DataChunk> streamData(StreamRequest request) {
        return dataService.streamData(request.getQuery())
            .onItem().transform(this::toChunk)
            // gRPC streams automatically support backpressure (HTTP/2 Flow Control)
            .onOverflow().buffer(256);
    }
}

#9. GraalVM Native Image

#9.1 Native Compilation Configuration

PROPERTIES
# application.properties
quarkus.native.enabled=true
quarkus.native.container-build=true
quarkus.native.builder-image=quay.io/quarkus/ubi-quarkus-mandrel-builder-image:jdk-21

quarkus.native.additional-build-args=\
  --initialize-at-run-time=io.netty.handler.ssl.BoringSSL,\
  -H:+ReportExceptionStackTraces

#9.2 Performance Comparison

MetricJVM ModeNative Mode
Startup time1.5s0.025s
First request latency200 ms5 ms
Memory footprint (RSS)180 MB35 MB
Peak throughput15,000 RPS12,000 RPS
Build time5s3 min

#9.3 Native Image Limitations and Workarounds

LimitationWorkaround
Reflection restrictedUse @RegisterForReflection
Dynamic proxy restrictedQuarkus ArC compile-time CDI
JNI restrictedAvoid JNI libraries
Serialization restrictedUse Quarkus serialization extensions

#10. Performance Benchmarks and Tuning

#10.1 Benchmark Results

Code
Test environment: 4 cores 8GB, PostgreSQL + Redis
Tool: wrk2, 60-second duration

Simple query (single instance GET):
  Quarkus Reactive: 28,000 RPS, P99 = 3.2ms
  Spring Boot MVC:   8,500 RPS, P99 = 12ms

Complex query (OQL + Join):
  Quarkus Reactive: 4,500 RPS, P99 = 45ms
  Spring Boot MVC:  1,800 RPS, P99 = 120ms

gRPC streaming (10,000 records):
  Quarkus Reactive: 2.1s to complete
  Traditional REST pagination: 8.5s to complete

#10.2 Key Tuning Parameters

PROPERTIES
# Connection pool
quarkus.datasource.reactive.max-size=20
quarkus.datasource.reactive.idle-removal-interval=5m

# HTTP server
quarkus.http.io-threads=8
quarkus.http.limits.max-body-size=10M
quarkus.http.idle-timeout=30s

# gRPC
quarkus.grpc.server.max-inbound-message-size=10485760
quarkus.grpc.server.handshake-timeout=10s

# Redis
quarkus.redis.max-pool-size=32
quarkus.redis.max-pool-waiting=64

#11. Key Takeaways

TopicKey Conclusion
Framework choiceData Layer uses Quarkus, Control Layer uses Spring Boot
Event loopNever block on IO threads; use @Blocking when necessary
MutinyUni for single-value async, Multi for stream async
DatabaseHibernate Reactive + Panache provides reactive ORM
gRPCQuarkus natively supports gRPC Server + Client
MessagingSmallRye Reactive Messaging integrates Kafka
Native60x faster startup, 80% less memory, but slower builds
Performance3-4x throughput improvement over Spring Boot

Next up: S8-15 dives into FastAPI + gRPC dual-protocol services, exploring how coomia-dip Intelligence Layer exposes both REST and gRPC interfaces simultaneously.