{"id":2098,"date":"2026-02-04T23:30:45","date_gmt":"2026-02-04T10:30:45","guid":{"rendered":"https:\/\/www.ronella.xyz\/?p=2098"},"modified":"2026-02-04T23:30:45","modified_gmt":"2026-02-04T10:30:45","slug":"oncompletion-and-unit-of-work-in-apache-camel-java-dsl","status":"publish","type":"post","link":"https:\/\/www.ronella.xyz\/?p=2098","title":{"rendered":"`onCompletion` and Unit of Work in Apache Camel (Java DSL)"},"content":{"rendered":"<p>In Java DSL, you declare routes in code, Camel wraps each Exchange in a Unit of Work, and <code>onCompletion<\/code> is the Java DSL hook that runs a sub\u2011route when that Unit of Work finishes.<\/p>\n<hr \/>\n<h2>Unit of Work: What Java DSL Developers Should Know<\/h2>\n<p>For each incoming Exchange, Camel creates a Unit of Work when it enters a route and tears it down when that route finishes processing the Exchange.<\/p>\n<ul>\n<li>It represents \u201cthis message\u2019s transaction\/lifecycle through this route.\u201d<\/li>\n<li>It manages completion callbacks via <code>Synchronization<\/code> hooks (e.g. <code>onComplete<\/code>, <code>onFailure<\/code>).<\/li>\n<li>In Java DSL, this is implicit; you usually just work with <code>Exchange<\/code> and let Camel manage the Unit of Work for you.<\/li>\n<\/ul>\n<p>You can still access it if you need to register low\u2011level callbacks:<\/p>\n<pre><code class=\"language-java\">from(&quot;direct:withUoW&quot;)\n    .process(exchange -&gt; {\n        var uow = exchange.getUnitOfWork();\n        \/\/ uow.addSynchronization(...);\n    })\n    .to(&quot;mock:result&quot;);<\/code><\/pre>\n<p><strong>Why this exists:<\/strong> Camel needs a clear lifecycle boundary to know when to commit transactions, acknowledge messages, and run \u201cfinished\u201d logic once per Exchange.<\/p>\n<hr \/>\n<h2>Core <code>onCompletion<\/code> Concept in Java DSL<\/h2>\n<p>The <code>onCompletion<\/code> DSL lets you attach a small route that runs when the original Exchange\u2019s Unit of Work completes.<\/p>\n<ul>\n<li>Camel takes a <strong>copy<\/strong> of the Exchange and routes it through the <code>onCompletion<\/code> block.<\/li>\n<li>This behaves \u201ckinda like a Wire Tap\u201d: the original thread can continue or finish while the completion route runs.<\/li>\n<\/ul>\n<p>Basic Java DSL example:<\/p>\n<pre><code class=\"language-java\">from(&quot;direct:start&quot;)\n    .onCompletion()\n        .to(&quot;log:on-complete&quot;)\n        .to(&quot;bean:auditBean&quot;)\n    .end()\n    .process(exchange -&gt; {\n        String body = exchange.getIn().getBody(String.class);\n        exchange.getMessage().setBody(&quot;Processed: &quot; + body);\n    })\n    .to(&quot;mock:result&quot;);<\/code><\/pre>\n<p><strong>Why use it:<\/strong> It centralizes \u201cafter everything is done\u201d logic (logging, metrics, notifications) instead of scattering it across processors and <code>finally<\/code> blocks.<\/p>\n<hr \/>\n<h2>Java DSL Scope: Global vs Route-Level<\/h2>\n<p>You can define <code>onCompletion<\/code> <strong>globally<\/strong> (context scope) or <strong>inside a specific route<\/strong> (route scope).<\/p>\n<h2>Global <code>onCompletion<\/code> (context scope)<\/h2>\n<p>Declared outside any <code>from(...)<\/code> in <code>configure()<\/code>:<\/p>\n<pre><code class=\"language-java\">@Override\npublic void configure() {\n\n    \/\/ Global onCompletion \u2013 applies to all routes unless overridden\n    onCompletion()\n        .onCompleteOnly()\n        .to(&quot;log:global-complete&quot;);\n\n    from(&quot;direct:one&quot;)\n        .to(&quot;bean:logicOne&quot;);\n\n    from(&quot;direct:two&quot;)\n        .to(&quot;bean:logicTwo&quot;);\n}<\/code><\/pre>\n<ul>\n<li>Runs for all routes in this <code>RouteBuilder<\/code> (and more generally in the context) when they complete successfully.<\/li>\n<\/ul>\n<h2>Route-level <code>onCompletion<\/code><\/h2>\n<p>Declared <em>inside<\/em> a route:<\/p>\n<pre><code class=\"language-java\">@Override\npublic void configure() {\n\n    \/\/ Global\n    onCompletion()\n        .onCompleteOnly()\n        .to(&quot;log:global-complete&quot;);\n\n    \/\/ Route-specific\n    from(&quot;direct:start&quot;)\n        .onCompletion().onFailureOnly()\n            .to(&quot;log:route-failed&quot;)\n        .end()\n        .process(exchange -&gt; {\n            String body = exchange.getIn().getBody(String.class);\n            if (body.contains(&quot;fail&quot;)) {\n                throw new IllegalStateException(&quot;Forced failure&quot;);\n            }\n            exchange.getMessage().setBody(&quot;OK: &quot; + body);\n        })\n        .to(&quot;mock:result&quot;);\n}<\/code><\/pre>\n<ul>\n<li>Route-level <code>onCompletion<\/code> overrides global <code>onCompletion<\/code> for that route\u2019s completion behavior.<\/li>\n<\/ul>\n<p><strong>Why the two levels:<\/strong> Global scope gives you cross\u2011cutting completion behavior (e.g. auditing for all routes), while route scope gives a particular route fine\u2011grained control.<\/p>\n<hr \/>\n<h2>Controlling When <code>onCompletion<\/code> Triggers (Java DSL)<\/h2>\n<p>The Java DSL offers fine control on when the completion logic is invoked.<\/p>\n<h2>Success vs failure<\/h2>\n<ul>\n<li>\n<p>Always (default): no extra flags; runs on both success and failure.<\/p>\n<\/li>\n<li>\n<p>Only on success:<\/p>\n<pre><code class=\"language-java\">from(\"direct:successOnly\")\n  .onCompletion().onCompleteOnly()\n      .to(\"log:success-only\")\n  .end()\n  .to(\"bean:logic\");<\/code><\/pre>\n<\/li>\n<li>\n<p>Only on failure:<\/p>\n<pre><code class=\"language-java\">from(\"direct:failureOnly\")\n  .onCompletion().onFailureOnly()\n      .to(\"log:failure-only\")\n  .end()\n  .process(exchange -> {\n      throw new RuntimeException(\"Boom\");\n  })\n  .to(\"mock:neverReached\");<\/code><\/pre>\n<\/li>\n<\/ul>\n<h2>Conditional with <code>onWhen<\/code><\/h2>\n<pre><code class=\"language-java\">from(&quot;direct:conditional&quot;)\n    .onCompletion().onWhen(body().contains(&quot;Alert&quot;))\n        .to(&quot;log:alert-completion&quot;)\n    .end()\n    .to(&quot;bean:normalProcessing&quot;);<\/code><\/pre>\n<ul>\n<li>The completion route runs only when the predicate evaluates to true on the completion Exchange (e.g. body contains <code>&quot;Alert&quot;<\/code>).<\/li>\n<\/ul>\n<p><strong>Why this is useful:<\/strong> It turns <code>onCompletion<\/code> into a small policy language: \u201cWhen we\u2019re done, if it failed do X; if it succeeded and condition Y holds, do Z.\u201d<\/p>\n<hr \/>\n<h2>Before vs After Consumer (InOut) in Java DSL<\/h2>\n<p>For request\u2013reply (InOut) routes, <code>onCompletion<\/code> can run either <strong>before<\/strong> or <strong>after<\/strong> the consumer writes the response back.<\/p>\n<h2>Default: after consumer (AfterConsumer)<\/h2>\n<pre><code class=\"language-java\">from(&quot;direct:service&quot;)\n    .onCompletion()\n        .to(&quot;log:after-response&quot;)\n    .end()\n    .process(exchange -&gt; {\n        exchange.getMessage().setBody(\n            &quot;Response for &quot; + exchange.getIn().getBody(String.class)\n        );\n    });<\/code><\/pre>\n<ul>\n<li>The caller receives the response; then the <code>onCompletion<\/code> sub\u2011route runs.<\/li>\n<\/ul>\n<h2>Before consumer: <code>modeBeforeConsumer()<\/code><\/h2>\n<pre><code class=\"language-java\">from(&quot;direct:serviceBefore&quot;)\n    .onCompletion().modeBeforeConsumer()\n        .setHeader(&quot;X-Processed-By&quot;, constant(&quot;MyService&quot;))\n        .to(&quot;log:before-response&quot;)\n    .end()\n    .process(exchange -&gt; {\n        exchange.getMessage().setBody(\n            &quot;Response for &quot; + exchange.getIn().getBody(String.class)\n        );\n    });<\/code><\/pre>\n<ul>\n<li><code>modeBeforeConsumer()<\/code> makes completion logic run before the consumer is done and before the response is written back, so you can still modify the outgoing message.<\/li>\n<\/ul>\n<p><strong>Why you\u2019d choose one:<\/strong><\/p>\n<ul>\n<li>Use <strong>AfterConsumer<\/strong> when completion work is purely side\u2011effect (logging, metrics, notifications).<\/li>\n<li>Use <strong>BeforeConsumer<\/strong> when completion should influence the final response (headers, correlation IDs, last\u2011minute logging of the exact response).<\/li>\n<\/ul>\n<hr \/>\n<h2>Synchronous vs Asynchronous <code>onCompletion<\/code> in Java DSL<\/h2>\n<p>Java DSL lets you decide whether completion runs sync or async.<\/p>\n<h2>Synchronous (default)<\/h2>\n<pre><code class=\"language-java\">from(&quot;direct:sync&quot;)\n    .onCompletion()\n        .to(&quot;log:sync-completion&quot;)\n    .end()\n    .to(&quot;bean:work&quot;);<\/code><\/pre>\n<ul>\n<li>Completion runs on the same thread; no extra thread pool is used by default (from Camel 2.14+).<\/li>\n<\/ul>\n<h2>Asynchronous with <code>parallelProcessing()<\/code><\/h2>\n<pre><code class=\"language-java\">from(&quot;direct:async&quot;)\n    .onCompletion().parallelProcessing()\n        .to(&quot;log:async-completion&quot;)\n        .to(&quot;bean:slowAudit&quot;)\n    .end()\n    .to(&quot;bean:fastBusiness&quot;);<\/code><\/pre>\n<ul>\n<li><code>parallelProcessing()<\/code> tells Camel to run the completion task asynchronously using a thread pool, so the original thread can complete sooner.<\/li>\n<\/ul>\n<h2>Custom executor with <code>executorServiceRef<\/code><\/h2>\n<pre><code class=\"language-java\">@Override\npublic void configure() {\n\n    \/\/ Assume &quot;onCompletionPool&quot; is registered in the CamelContext\n    from(&quot;direct:customPool&quot;)\n        .onCompletion()\n            .parallelProcessing()\n            .executorServiceRef(&quot;onCompletionPool&quot;)\n            .to(&quot;bean:expensiveLogger&quot;)\n        .end()\n        .to(&quot;bean:mainLogic&quot;);\n}<\/code><\/pre>\n<p><strong>Why async:<\/strong> Completion tasks are often slow but non\u2011critical (e.g. writing to external systems), so running them on a separate thread avoids holding up the main request path.<\/p>\n<hr \/>\n<h2>How <code>onCompletion<\/code> and Unit of Work Fit Together (Java DSL View)<\/h2>\n<p>Bringing it together for Java DSL:<\/p>\n<ul>\n<li>Each Exchange in a route is wrapped in a Unit of Work; that\u2019s the lifecycle boundary.<\/li>\n<li>At the end of that lifecycle, Camel fires completion hooks (<code>Synchronization<\/code>) and also activates any <code>onCompletion<\/code> routes defined in Java DSL.<\/li>\n<li><code>onCompletion<\/code> routes receive a copy of the Exchange and usually do not affect the already\u2011completed main route, especially when run async.<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>In Java DSL, you declare routes in code, Camel wraps each Exchange in a Unit of Work, and onCompletion is the Java DSL hook that runs a sub\u2011route when that Unit of Work finishes. Unit of Work: What Java DSL Developers Should Know For each incoming Exchange, Camel creates a Unit of Work when it [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[92],"tags":[],"_links":{"self":[{"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/posts\/2098"}],"collection":[{"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=2098"}],"version-history":[{"count":1,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/posts\/2098\/revisions"}],"predecessor-version":[{"id":2099,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/posts\/2098\/revisions\/2099"}],"wp:attachment":[{"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=2098"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=2098"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=2098"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}