Apache Camel is a powerful open-source integration framework that lets you connect systems, transform data, and automate workflows without reinventing the wheel. In this lesson, you'll build your very first Camel route — a file-to-log pipeline that reads a file from disk and prints its contents to the console. By the end, you'll understand the core building blocks of Camel and have the confidence to wire up your own integrations.
Before writing any code, it's worth understanding what Camel actually does — because it's easy to mistake it for a messaging library or a framework, when it's really more like a universal translator for systems.
Imagine you're a logistics manager. You have packages arriving from different carriers (FTP servers, HTTP APIs, message queues, file systems) and they all need to end up in the right warehouse (databases, email systems, log files, microservices). Camel is the logistics network in between: it picks up the package, optionally repackages or relabels it, and delivers it to the destination — all based on rules you define.
The fundamental concept in Camel is the route: a declaration of where data comes from, what happens to it along the way, and where it ends up. Routes are defined using Camel's Domain-Specific Language (DSL), which is available in Java, XML, YAML, and other formats.
Under the hood, Camel uses components — pluggable adapters for hundreds of systems. There's a file component for the file system, a log component for logging, an http component for REST calls, and over 300 more. You don't write network code or file I/O code yourself — you just declare what you want connected.
Note: Camel is not a message broker like RabbitMQ or Kafka. It's an integration framework — it connects things together. You can even use Camel to connect to Kafka.
The mental model is: consumer → processor(s) → producer. A consumer reads from a source, processors transform or enrich the data, and a producer writes to a destination. This pipeline is your route.
The fastest way to get Camel running is with Spring Boot and Maven. Spring Boot handles startup and dependency injection, while Maven manages your libraries.
Start by creating a pom.xml with the Camel Spring Boot starter and the components you need:
<dependencies>
<!-- Spring Boot base -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Apache Camel Spring Boot starter -->
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-spring-boot-starter</artifactId>
<version>4.4.0</version>
</dependency>
<!-- File component (usually bundled in camel-core) -->
<!-- Log component (also in camel-core) -->
</dependencies>
Both the file and log components ship with camel-core, so no extra dependencies are needed for this lesson. If you want to use other components later (like camel-http or camel-kafka), you'd add them individually.
Your main application class is standard Spring Boot:
@SpringBootApplication
public class CamelDemoApplication {
public static void main(String[] args) {
SpringApplication.run(CamelDemoApplication.class, args);
}
}
Camel integrates with Spring Boot through auto-configuration — the moment Spring Boot sees the Camel starter on the classpath, it creates a CamelContext (Camel's runtime container) automatically. You don't need to start or stop it manually.
Note: The
CamelContextis the heart of Camel. It holds all your routes, components, and configurations. Think of it as the runtime engine that keeps everything running.
One useful Spring Boot property to add to application.properties is:
camel.springboot.main-run-controller=true
This tells Camel to keep the application alive so routes can keep running, rather than shutting down immediately after startup.
from, to, and the DSLNow let's look at the Java DSL — the most common way to define routes in Camel. Every route you write will extend the RouteBuilder class and override the configure() method. Inside configure(), you chain method calls to describe your data flow.
The two most fundamental methods are:
from("component:options") — defines the consumer (where data is read from)to("component:options") — defines the producer (where data is written to)Between them, you can chain steps like .log(), .transform(), .filter(), .process(), and more.
Here's the skeletal structure:
@Component
public class MyRoute extends RouteBuilder {
@Override
public void configure() {
from("source-uri")
.log("Received: ${body}")
.to("destination-uri");
}
}
The string you pass to from() and to() is called a Camel endpoint URI. It follows the format component:path?option1=value1&option2=value2. For example:
file:data/input?noop=true — reads files from the data/input directorylog:myLogger?level=INFO — logs to a named logger at INFO levelThe ${body} syntax inside .log() is Simple Language — Camel's built-in expression language for accessing message data. ${body} refers to the current message payload (the file content, in our case).
Note: Camel URIs look like URLs but they're not. The part before the colon is the component name, and everything after is configuration. Always check the component documentation for available options.
Let's now write the actual route. The goal: watch a folder called data/input, pick up any .txt file that appears, and log its contents to the console.
@Component
public class FileToLogRoute extends RouteBuilder {
@Override
public void configure() {
from("file:data/input?noop=true&include=.*\\.txt")
.routeId("file-to-log-route")
.log("📄 File received: ${header.CamelFileName}")
.log("📝 Contents: ${body}")
.to("log:file-output?level=INFO&showBody=true");
}
}
Let's walk through each line:
from("file:data/input?noop=true&include=.*\\.txt") — Polls the data/input directory every second (default). noop=true means files stay in place after processing. include=.*\\.txt filters to only .txt files.
.routeId("file-to-log-route") — Assigns a human-readable name. Not required, but invaluable for debugging and monitoring.
.log("📄 File received: ${header.CamelFileName}") — Logs the filename. ${header.CamelFileName} accesses a special Camel header automatically set by the file component — no manual work needed.
.log("📝 Contents: ${body}") — Logs the actual file content. The file body is automatically read as a String.
.to("log:file-output?level=INFO&showBody=true") — Sends the message to Camel's log component, which writes to SLF4J/Logback under the logger name file-output.
Now create the directory data/input in your project root, start the application, and drop a .txt file in. Within a second you'll see the output in your console.
noop=true option do in the file component URI?When Camel picks up a file, it doesn't just pass around raw bytes. It wraps everything in an Exchange — a container object that holds the message data plus a rich set of metadata.
An Exchange has two main slots:
In message — the current incoming message (has a body, headers, and attachments)Out message — used in request-reply scenarios (less common for file routes)The body is the actual payload (your file content as a String or byte array). Headers are key-value metadata attached to the message. The file component automatically populates useful headers:
| Header | Value |
|---|---|
| CamelFileName | The name of the file (e.g., hello.txt) |
| CamelFileAbsolutePath | Full absolute path to the file |
| CamelFileLastModified | Last modification timestamp |
| CamelFileLength | File size in bytes |
You access headers in Simple Language with ${header.HeaderName} or in Java code with exchange.getIn().getHeader("CamelFileName", String.class).
You can also add your own headers using .setHeader():
from("file:data/input?noop=true")
.setHeader("processedBy", constant("FileToLogRoute"))
.log("Processed by: ${header.processedBy} — File: ${header.CamelFileName}");
This pattern is powerful when you need to pass context between route steps or downstream systems. Think of headers as sticky notes attached to the package — they travel with the message the whole way through.
Note: The Exchange also has properties (similar to headers but scoped to the Exchange, not the message) and an exception field used in error handling. You'll encounter these in more advanced routes.
Routes become genuinely useful when you transform or react to data — not just pass it through. Let's add a .process() step that modifies the message body before logging it.
A Processor is a single unit of work in Camel. You implement the Processor interface (a @FunctionalInterface with one method: process(Exchange exchange)), which gives you full access to the Exchange.
@Component
public class FileToLogRoute extends RouteBuilder {
@Override
public void configure() {
from("file:data/input?noop=true&include=.*\\.txt")
.routeId("file-to-log-route")
.log("File received: ${header.CamelFileName}")
.process(exchange -> {
String originalBody = exchange.getIn().getBody(String.class);
String upperBody = originalBody.toUpperCase();
exchange.getIn().setBody(upperBody);
})
.log("Transformed content: ${body}")
.to("log:file-output?level=INFO");
}
}
The lambda exchange -> { ... } is a processor. We read the body with getBody(String.class) (Camel automatically converts the file content to a String), transform it, and write it back with setBody().
You can also extract processors into their own classes for reuse:
@Component
public class UpperCaseProcessor implements Processor {
@Override
public void process(Exchange exchange) throws Exception {
String body = exchange.getIn().getBody(String.class);
exchange.getIn().setBody(body.toUpperCase());
}
}
Then inject and use it in your route:
@Autowired
private UpperCaseProcessor upperCaseProcessor;
// in configure():
.process(upperCaseProcessor)
This separation makes routes easier to test — you can unit test UpperCaseProcessor independently without starting a full Camel context.
Note: Camel also provides a
.transform()DSL method for simple body transformations:.transform(body().convertToString().append(" [processed]")). Use.process()when you need complex logic; use.transform()for simple expressions.
from) to a producer (to), with optional processing steps in betweencomponent:path?option=value and are how you configure sources and destinationsCamelFileName and CamelFileAbsolutePath — no manual setup needed.routeId() on every route makes logs, monitoring, and debugging dramatically easierApache Camel uses the mental model of "consumer → processor(s) → producer" to describe how data flows through a route. In your own words, explain what each of these three roles does in the context of a Camel route, using the file-to-log pipeline from this lesson as a concrete example. Walk through what happens at each stage — from the moment a file appears on disk to when its contents are printed to the console — and explain why this separation of concerns is useful when building integrations.