Building a Real-Time RSS Integration with Quarkus and Apache Camel
Learn how to stream, transform, and monitor RSS feeds using Camel DSL, Micrometer metrics, and Quarkus for production-ready Java integrations.
We will rebuild Spring’s “Integrating Data” starter in Quarkus using Apache Camel’s Java DSL. The route polls the Quarkus blog RSS feed, splits entries, transforms them to a POJO, writes newline-delimited JSON, and exposes an optional REST endpoint to read recent items.
Camel gives you a stable DSL and connectors. Quarkus gives you fast dev loops and production efficiency. Together they make small, reliable integration services.
Prerequisites
JDK 21
Maven 3.9+ or the Quarkus CLI installed (
quarkus --version
)
Project bootstrap
quarkus create app org.acme:camel-rss-quarkus:1.0.0 \
-x "camel-quarkus-rss,camel-quarkus-file,camel-quarkus-log,camel-quarkus-jackson,camel-quarkus-direct,quarkus-rest-jackson,camel-quarkus-micrometer"
cd camel-rss-quarkus
Why these extensions:
camel-quarkus-rss
to poll and parse RSS.camel-quarkus-jackson
for JSON.camel-quarkus-file
to write to disk.camel-quarkus-log
for quick visibility.quarkus-rest-jackson
for the tiny verification endpoint.camel-quarkus-micrometer
for health metrics
Just wanna take a sneak peak at the code? Look at the example Github repository.
Configuration
We keep the source and sink URIs in properties. This makes the route easy to test by swapping endpoints.
# src/main/resources/application.properties
# output location for newline-delimited JSON
app.out.dir=target/out
app.out.filename=quarkus-feed.jsonl
# default source and sink for real runs
app.source.uri=rss:https://quarkus.io/feed.xml
app.sink.uri=file:${app.out.dir}?fileName=${app.out.filename}&fileExist=Append
# logs
quarkus.log.category.”org.apache.camel”.level=INFO
quarkus.log.console.json=false
Domain model
// src/main/java/org/acme/rss/FeedItem.java
package org.acme.rss;
import java.time.OffsetDateTime;
public record FeedItem(
String title,
String link,
String author,
OffsetDateTime published
) {}
Short, immutable, JSON-friendly.
The Camel route (Java DSL)
The route polls the RSS feed, splits into entries, maps to FeedItem
, marshals to JSON, and appends to a file. We inject Quarkus’ ObjectMapper
so Jackson modules are shared.
package org.acme.rss;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.jackson.JacksonDataFormat;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rometools.rome.feed.synd.SyndEntry;
import com.rometools.rome.feed.synd.SyndFeed;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped
public class RssRoute extends RouteBuilder {
@Inject
public ObjectMapper mapper;
// Track processed items to avoid duplicate counting
private final Set<String> processedItems = ConcurrentHashMap.newKeySet();
@Override
public void configure() {
JacksonDataFormat json = new JacksonDataFormat(mapper, FeedItem.class);
from(”{{app.source.uri}}?splitEntries=false&delay=60000”)
.routeId(”quarkus-rss-poller”)
.log(”Fetched RSS feed: ${body.title}”)
.process(exchange -> {
Object body = exchange.getMessage().getBody();
if (body instanceof SyndFeed) {
SyndFeed feed = (SyndFeed) body;
log.info(”Processing feed: “ + feed.getTitle() + “ with “ + feed.getEntries().size()
+ “ entries”);
// Process each entry in the feed
int processedCount = 0;
int skippedCount = 0;
for (SyndEntry entry : feed.getEntries()) {
String author = entry.getAuthor() != null ? entry.getAuthor() : “unknown”;
String month = “unknown”;
if (entry.getPublishedDate() != null) {
OffsetDateTime publishedDate = entry.getPublishedDate().toInstant()
.atOffset(java.time.ZoneOffset.UTC);
month = publishedDate.format(DateTimeFormatter.ofPattern(”yyyy-MM”));
}
// Create a unique key for deduplication (title + link + published date)
String uniqueKey = entry.getTitle() + “|” + entry.getLink() + “|” +
(entry.getPublishedDate() != null ? entry.getPublishedDate().getTime() : “no-date”);
// Skip if already processed
if (processedItems.contains(uniqueKey)) {
log.info(”Skipping already processed item: “ + entry.getTitle());
skippedCount++;
continue;
}
// Mark as processed
processedItems.add(uniqueKey);
processedCount++;
FeedItem item = new FeedItem(
entry.getTitle(),
entry.getLink(),
author,
entry.getPublishedDate() != null
? entry.getPublishedDate().toInstant().atOffset(java.time.ZoneOffset.UTC)
: null);
// Create a new exchange for each entry with metrics headers
exchange.getContext().createProducerTemplate().sendBodyAndHeaders(”direct:processEntry”,
item,
java.util.Map.of(”author”, author, “month”, month));
}
// Clear the body since we’ve processed all entries
exchange.getMessage().setBody(null);
log.info(”Feed processing complete: “ + processedCount + “ new items processed, “ + skippedCount + “ items skipped”);
} else {
log.warn(”Expected SyndFeed but got: “ + (body != null ? body.getClass().getName() : “null”));
exchange.getMessage().setBody(null);
}
})
.log(”Finished processing feed”);
from(”direct:processEntry”)
.marshal(json)
.convertBodyTo(String.class)
.setBody(simple(”${body}”))
.transform(body().append(”\n”))
.toD(”{{app.sink.uri}}”)
.to(”micrometer:counter:rss_items_total?tags=author=${header.author},month=${header.month}”)
.log(”Wrote entry to sink”);
}
}
Key Components
splitEntries=false: The entire list of entries from the current feed is set in the exchange.
delay=60000: Polls every 60 seconds (1 minute)
Deduplication: Camel RSS consumer automatically deduplicates already seen entries
Feed Processing Logic
Input* Receives `SyndFeed` objects from RSS component
Processing: Iterates through each `SyndEntry` in the feed
Data Extraction
Author (with “unknown” fallback)
Publication month (YYYY-MM format)
Title, link, and published date
Entry Processing Route
JSON Serialization: Converts `FeedItem` to JSON string
File Writing: Appends to output file with newline delimiter
Metrics Collection: Records counter with author and month tags
Metrics Integration
Counter: `rss_items_total` tracks processed entries
Tags: `author` and `month` for dimensional analysis
Headers: Author and month stored as exchange headers for micrometer
Optional REST endpoint to read latest items
This reads the JSONL file and returns the last N items. Helpful to verify the flow.
package org.acme;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.acme.rss.FeedItem;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.quarkus.logging.Log;
import jakarta.inject.Inject;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
@jakarta.ws.rs.Path(”/feed”)
@Produces(MediaType.APPLICATION_JSON)
public class FeedResource {
@ConfigProperty(name = “app.out.filename”)
String outFilename;
@Inject
private ObjectMapper mapper;
private Path getOutFile() {
return Path.of(”target/out”, outFilename);
}
@GET
public List<FeedItem> latest(@QueryParam(”limit”) @DefaultValue(”5”) int limit) throws IOException {
Path outFile = getOutFile();
if (!Files.exists(outFile))
return List.of();
List<String> lines = Files.readAllLines(outFile);
if (lines.isEmpty()) {
return List.of();
}
List<FeedItem> items = new ArrayList<>();
for (String line : lines) {
if (line.trim().isEmpty())
continue;
try {
items.add(mapper.readValue(line.trim(), FeedItem.class));
} catch (Exception e) {
Log.error(”Failed to parse JSON: “ + line.substring(0, Math.min(100, line.length())));
}
}
// Get the last ‘limit’ items
int from = Math.max(0, items.size() - limit);
List<FeedItem> result = items.subList(from, items.size());
Collections.reverse(result); // newest first
return result;
}
}
Run and verify
Start dev mode:
quarkus dev
Watch the logs. You should see entries polled and “Wrote item to sink.” A file appears at target/out/quarkus-feed.jsonl
.
Verify with curl:
curl http://localhost:8080/feed
curl http://localhost:8080/feed?limit=10
If no new data shows up for a while, the consumer likely deduped unchanged entries. That is expected.
Verify metrics
Check counters at the metrics endpoint:
curl http://localhost:8080/q/metrics | grep rss_items_total
You’ll see something like:
rss_items_total{author=”Guillaume Smet (https://twitter.com/gsmet_)”,camelContext=”camel-1”,month=”2025-07”} 64.0
rss_items_total{author=”Guillaume Smet (https://twitter.com/gsmet_)”,camelContext=”camel-1”,month=”2025-08”} 48.0
rss_items_total{author=”Guillaume Smet (https://twitter.com/gsmet_)”,camelContext=”camel-1”,month=”2025-09”} 32.0
rss_items_total{author=”Luca Molteni (https://twitter.com/volothamp)”,camelContext=”camel-1”,month=”2025-06”} 8.0
I have included a super small metrics.html page in the Github repository that displays some statistics directly from the health endpoint:
Congratulations to Guillaume Smet for publishing both the most posts on the Quarkus Blog and also the most posts in a single month :)
Production notes
Tune
consumer.delay
based on upstream rate and your SLOs. Start conservative.Use durable sinks for downstream processing. Replace
file:
with Kafka, S3, JDBC, or Elasticsearch by switching the endpoint, not the business logic.For native images, verify the Camel Quarkus extensions you use are supported and run your tests against the native binary.
Wrapping Up
In this tutorial you built a complete RSS integration service with Quarkus and Camel. Starting from the Quarkus blog feed, you learned how to:
Use the Camel
rss:
component to poll and split feed entries.Transform raw
SyndEntry
objects into a cleanFeedItem
record.Log and persist entries as newline-delimited JSON files.
Expose a simple REST endpoint to read the processed items.
Extend the route with Camel’s Micrometer component to capture real-time statistics on authors and publications per month, exportable to Prometheus and Grafana.
Together these steps mirror the flow from Spring’s integration guide while showing how Quarkus and Camel make the same patterns lightweight, observable, and production-ready. You now have a working foundation you can evolve further: Swap the file sink for Kafka or a database, enrich feeds with extra processors, or build dashboards around the Micrometer metrics.
A small, fast Quarkus service, an expressive Camel DSL, and enterprise-grade observability. That’s a solid recipe for integration work done right.
Small, focused, and ready to extend.