<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Luis Iñesta Gelabert</title>
    <description>The latest articles on DEV Community by Luis Iñesta Gelabert (@luiinge).</description>
    <link>https://dev.to/luiinge</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F573604%2Fbadaa157-6103-4547-9cee-3d1df5893c83.png</url>
      <title>DEV Community: Luis Iñesta Gelabert</title>
      <link>https://dev.to/luiinge</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/luiinge"/>
    <language>en</language>
    <item>
      <title>Azertio: API and Database Testing Without the Glue Code</title>
      <dc:creator>Luis Iñesta Gelabert</dc:creator>
      <pubDate>Sun, 17 May 2026 17:37:07 +0000</pubDate>
      <link>https://dev.to/luiinge/azertio-api-and-database-testing-without-the-glue-code-1ibl</link>
      <guid>https://dev.to/luiinge/azertio-api-and-database-testing-without-the-glue-code-1ibl</guid>
      <description>&lt;p&gt;If you have ever maintained a Cucumber + RestAssured test suite, you know the feeling. The feature files look clean. But underneath there are dozens of step definition classes, &lt;code&gt;ScenarioContext&lt;/code&gt; maps to pass state between steps, &lt;code&gt;@Before&lt;/code&gt; hooks to wire up the HTTP client, and a &lt;code&gt;pom.xml&lt;/code&gt; that has grown to include JDBC drivers, connection pools, and fixtures loaders just to back up a few database assertions.&lt;/p&gt;

&lt;p&gt;The tests work. But they are a project in themselves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Azertio&lt;/strong&gt; is a new open-source testing tool that takes a different approach: instead of writing glue code, you declare plugins. Instead of managing a build file, you configure a YAML file. Instead of parsing JUnit XML reports, you browse a live execution history in your IDE.&lt;/p&gt;

&lt;p&gt;Let me walk you through what it does and how it is designed.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: Tests That Require Too Much Code
&lt;/h2&gt;

&lt;p&gt;BDD tools like Cucumber are built around a promise: business stakeholders write the test scenarios, developers implement the steps. In practice this rarely holds. Every new step a tester wants to use requires a developer to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write a Java method annotated with &lt;code&gt;@Given&lt;/code&gt;, &lt;code&gt;@When&lt;/code&gt;, or &lt;code&gt;@Then&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Wire it to a Gherkin expression via a regex&lt;/li&gt;
&lt;li&gt;Manage shared state between steps&lt;/li&gt;
&lt;li&gt;Register configuration in a Spring context or a custom hook&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result is that feature files — the part everyone can read — are tightly coupled to a growing body of Java infrastructure that almost nobody reads. And this infrastructure is per-project: if three teams test REST APIs, each team writes their own &lt;code&gt;iMakeGetRequest&lt;/code&gt; and &lt;code&gt;statusCodeIs&lt;/code&gt; methods.&lt;/p&gt;

&lt;p&gt;Azertio's premise is simple: &lt;strong&gt;the most common test steps should already exist as reusable, versioned plugins, and you should not need to write any Java to use them.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;An Azertio project consists of two things: an &lt;code&gt;azertio.yaml&lt;/code&gt; configuration file and your &lt;code&gt;.feature&lt;/code&gt; files. That's it — no &lt;code&gt;pom.xml&lt;/code&gt;, no step definition classes, no build tool.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# azertio.yaml&lt;/span&gt;
&lt;span class="na"&gt;testProject&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;organization&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Acme Corp&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Payment Service Tests&lt;/span&gt;
  &lt;span class="na"&gt;test-suites&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;smoke&lt;/span&gt;
      &lt;span class="na"&gt;tag-expression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;smoke"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;regression&lt;/span&gt;
      &lt;span class="na"&gt;tag-expression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;regression&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;or&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;smoke"&lt;/span&gt;

&lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;gherkin&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rest&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db with org.postgresql:postgresql-42.7.3&lt;/span&gt;

&lt;span class="na"&gt;configuration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;rest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{base-url}}/api"&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10000&lt;/span&gt;

&lt;span class="na"&gt;profiles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;dev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;base-url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:8080&lt;/span&gt;
  &lt;span class="na"&gt;staging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;base-url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://staging.example.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you install the plugins (downloaded from Maven Central and cached locally):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;azertio &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And run your tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;azertio run &lt;span class="nt"&gt;-s&lt;/span&gt; smoke &lt;span class="nt"&gt;-p&lt;/span&gt; staging
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your feature files can use REST and database steps immediately, with no backing code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gherkin"&gt;&lt;code&gt;&lt;span class="kn"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; Creating a payment persists it in the database
  &lt;span class="err"&gt;When I make a POST request to "payments" with body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;"""json
    { "amount": 49.99, "currency": "EUR", "card": "4111111111111111" }
    """&lt;/span&gt;
  &lt;span class="nf"&gt;Then &lt;/span&gt;the HTTP status code is equal to 201
  &lt;span class="nf"&gt;And &lt;/span&gt;I store the value of field &lt;span class="s"&gt;"id"&lt;/span&gt; from the response body into variable paymentId
  &lt;span class="nf"&gt;* &lt;/span&gt;use db &lt;span class="s"&gt;"main"&lt;/span&gt;
  &lt;span class="err"&gt;* db query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;"""sql
    SELECT status FROM payments WHERE id = '${paymentId}'
    """&lt;/span&gt;
  &lt;span class="nf"&gt;* &lt;/span&gt;db query count = 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Variable interpolation (&lt;code&gt;${paymentId}&lt;/code&gt;) is first-class. No custom parameter types, no manual string replacement.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture: Plugins All the Way Down
&lt;/h2&gt;

&lt;p&gt;Every capability in Azertio is a plugin — including the Gherkin parser, the REST steps, and the database steps. Plugins are standard Maven artifacts loaded at runtime via the &lt;strong&gt;Java Platform Module System (JPMS)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This has concrete consequences:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No classpath pollution.&lt;/strong&gt; Each plugin runs in its own module layer. A plugin's dependencies cannot interfere with the core runtime or with other plugins. You can have two plugins that depend on different versions of the same library without conflict.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runtime dependency declaration.&lt;/strong&gt; Need to test a MySQL database? Add &lt;code&gt;db with com.mysql:mysql-connector-j&lt;/code&gt; to &lt;code&gt;azertio.yaml&lt;/code&gt;. The JDBC driver is downloaded from Maven Central and loaded into the plugin's module layer at runtime. No &lt;code&gt;pom.xml&lt;/code&gt; edit required, because there is no &lt;code&gt;pom.xml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;True extensibility.&lt;/strong&gt; Writing a custom step plugin is straightforward: implement &lt;code&gt;StepProvider&lt;/code&gt;, annotate methods with &lt;code&gt;@StepExpression&lt;/code&gt;, and publish the artifact to any Maven repository. Teams that consume it just add one line to &lt;code&gt;azertio.yaml&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Extension&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"My Protocol"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Scope&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TRANSIENT&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyStepProvider&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;StepProvider&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@StepExpression&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"my.connect"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"host:text"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"port:integer"&lt;/span&gt;&lt;span class="o"&gt;})&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// your implementation&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The consuming project gets it automatically on the next &lt;code&gt;azertio install&lt;/code&gt; — no code changes, no dependency management.&lt;/p&gt;




&lt;h2&gt;
  
  
  Benchmark Mode: Performance Testing Without a Separate Tool
&lt;/h2&gt;

&lt;p&gt;One problem with specialized testing tools is proliferation. You have one tool for functional tests (Cucumber), one for performance (Gatling or k6), and you end up maintaining the same test logic in two places.&lt;/p&gt;

&lt;p&gt;Azertio has a built-in benchmark mode that works on any compatible step in the same &lt;code&gt;.feature&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gherkin"&gt;&lt;code&gt;&lt;span class="kn"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; Functional — retrieve payment
  &lt;span class="nf"&gt;When &lt;/span&gt;I make a GET request to &lt;span class="s"&gt;"payments/1"&lt;/span&gt;
  &lt;span class="nf"&gt;Then &lt;/span&gt;the HTTP status code is equal to 200

&lt;span class="kn"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; Performance — retrieve payment meets SLA
  &lt;span class="nf"&gt;Given &lt;/span&gt;benchmark mode is enabled with 500 executions and 16 threads
  &lt;span class="nf"&gt;When &lt;/span&gt;I make a GET request to &lt;span class="s"&gt;"payments/1"&lt;/span&gt;
  &lt;span class="nf"&gt;Then &lt;/span&gt;the benchmark P95 response time (ms) is less than 150
  &lt;span class="nf"&gt;Then &lt;/span&gt;the benchmark error rate is equal to 0.0
  &lt;span class="nf"&gt;Then &lt;/span&gt;the benchmark throughput (req/s) is greater than 200.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Benchmark runs use virtual threads for high concurrency with low resource overhead. Statistics (min, max, mean, P50, P95, P99, throughput, error rate) are stored with the execution and visible in the VS Code extension alongside functional results. If the P95 threshold is breached, the CI build fails — no Gatling server, no separate pipeline stage.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Definition / Implementation Model
&lt;/h2&gt;

&lt;p&gt;One of Azertio's most distinctive features is a two-level scenario model that has no direct equivalent in other tools.&lt;/p&gt;

&lt;p&gt;In most BDD projects, feature files serve two masters at once: business stakeholders who need to validate that tests reflect real requirements, and engineers who need steps precise enough to execute. These two needs pull in opposite directions, and the result is usually a compromise that satisfies neither.&lt;/p&gt;

&lt;p&gt;Azertio solves this with a formal separation. A &lt;strong&gt;definition&lt;/strong&gt; feature (tagged &lt;code&gt;@definition&lt;/code&gt;) contains the abstract, business-readable test intent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gherkin"&gt;&lt;code&gt;&lt;span class="nt"&gt;@definition&lt;/span&gt;
&lt;span class="kd"&gt;Feature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; User Registration

@ID-REG-01
&lt;span class="kn"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;A &lt;/span&gt;new user can register with valid data
  &lt;span class="nf"&gt;Given &lt;/span&gt;a valid registration form
  &lt;span class="nf"&gt;When &lt;/span&gt;the form is submitted
  &lt;span class="nf"&gt;Then &lt;/span&gt;the account is created
  &lt;span class="nf"&gt;And &lt;/span&gt;a welcome email is sent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An &lt;strong&gt;implementation&lt;/strong&gt; feature (tagged &lt;code&gt;@implementation&lt;/code&gt;) contains the concrete, executable steps, matched to the definition by identifier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gherkin"&gt;&lt;code&gt;&lt;span class="nt"&gt;@implementation&lt;/span&gt;
&lt;span class="kd"&gt;Feature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; User Registration — REST

&lt;span class="c"&gt;# gherkin.step-map: 1-1-1-1&lt;/span&gt;
@ID-REG-01
&lt;span class="kn"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;A &lt;/span&gt;new user can register with valid data
  &lt;span class="err"&gt;When I make a POST request to "users" with body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;"""json
    { "name": "Alice", "email": "alice@example.com" }
    """&lt;/span&gt;
  &lt;span class="nf"&gt;Then &lt;/span&gt;the HTTP status code is equal to 201
  &lt;span class="err"&gt;And the response body contains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;"""json
    { "email": "alice@example.com" }
    """&lt;/span&gt;
  &lt;span class="nf"&gt;And &lt;/span&gt;the response body field &lt;span class="s"&gt;"welcomeEmailSent"&lt;/span&gt; is equal to &lt;span class="s"&gt;"true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At plan-build time the two files are merged. The result tree shows the definition structure — business-readable, approvable in a pull request — while executing the implementation steps underneath each abstract step. Stakeholders see the definition; the framework runs the implementation.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;gherkin.step-map&lt;/code&gt; comment controls how many implementation steps correspond to each abstract definition step, including &lt;code&gt;0&lt;/code&gt; for steps that should appear in the result tree but not execute.&lt;/p&gt;

&lt;p&gt;This model is particularly useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Regulatory traceability&lt;/strong&gt; — the definition is the signed-off specification; the implementation is the audit trail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multilingual teams&lt;/strong&gt; — definition in the business language, implementation in the team's preferred language.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protocol evolution&lt;/strong&gt; — one definition, multiple implementations (REST today, gRPC tomorrow).&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Execution History and VS Code Integration
&lt;/h2&gt;

&lt;p&gt;A persistent frustration with most testing tools is that execution history vanishes. JUnit XML reports go into &lt;code&gt;target/&lt;/code&gt;, CI artifacts expire after 30 days, and there is no way to compare last Thursday's run with today's without a separate Allure or ReportPortal setup.&lt;/p&gt;

&lt;p&gt;Azertio has a built-in persistence layer with three modes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Backend&lt;/th&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;transient&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Temp HSQLDB (deleted on exit)&lt;/td&gt;
&lt;td&gt;CI pipelines that only need pass/fail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HSQLDB file in &lt;code&gt;.azertio/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Developer workstation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;remote&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PostgreSQL + MinIO&lt;/td&gt;
&lt;td&gt;Shared team history&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In &lt;code&gt;remote&lt;/code&gt; mode, every CI run writes the full execution tree — plan, suites, scenarios, steps, timings, and binary attachments — to a shared PostgreSQL database. Every developer's VS Code extension connects to the same backend and can browse, inspect, and re-run any past execution from any branch or CI run.&lt;/p&gt;

&lt;p&gt;The VS Code extension connects to the CLI via a JSON-RPC server and provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Execution history&lt;/strong&gt; with date, duration, and pass/fail status&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result tree navigation&lt;/strong&gt; from suite down to individual steps with timings&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attachment inspection&lt;/strong&gt; — response bodies, CSV query results, any data produced by steps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-click re-run&lt;/strong&gt; of any past execution against its original plan and profile&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inline benchmark statistics&lt;/strong&gt; alongside functional results&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No Allure server. No external dashboard. No configuration beyond the persistence block in &lt;code&gt;azertio.yaml&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Comparison: How Azertio Fits Against Other Tools
&lt;/h2&gt;

&lt;h3&gt;
  
  
  vs. Cucumber + RestAssured
&lt;/h3&gt;

&lt;p&gt;Cucumber requires glue code for every step — a Java method annotated with &lt;code&gt;@Given&lt;/code&gt;/&lt;code&gt;@When&lt;/code&gt;/&lt;code&gt;@Then&lt;/code&gt;. RestAssured provides the HTTP layer, but state management between steps, configuration wiring, and especially database assertions are all your problem.&lt;/p&gt;

&lt;p&gt;Azertio eliminates glue code entirely. The &lt;code&gt;rest&lt;/code&gt; and &lt;code&gt;db&lt;/code&gt; plugins cover the majority of API and database testing scenarios without writing any Java. When you do need custom steps, you write a plugin once and reuse it across all projects.&lt;/p&gt;

&lt;p&gt;The tradeoff: if your tests are tightly coupled to a Spring Boot application context (accessing internal services, using transactional rollback between scenarios), Cucumber + RestAssured remains the right tool. Azertio is explicitly a black-box tool and does not access application internals.&lt;/p&gt;

&lt;h3&gt;
  
  
  vs. Karate
&lt;/h3&gt;

&lt;p&gt;Karate embeds a JavaScript engine inside &lt;code&gt;.feature&lt;/code&gt; files. You can call Java, write loops, and use conditional logic directly in feature files — which is powerful, but it means tests become mini-programs that non-technical stakeholders cannot read or validate.&lt;/p&gt;

&lt;p&gt;Azertio enforces a clean boundary: feature files contain only declarative steps, all logic lives in typed Java step providers. The configuration model is also fundamentally different — Karate uses a &lt;code&gt;karate-config.js&lt;/code&gt; JavaScript file for environments, while Azertio uses pure YAML profiles. Azertio also has first-class features that Karate lacks: the definition/implementation model, built-in persistence, and a dedicated VS Code extension.&lt;/p&gt;

&lt;p&gt;The tradeoff: Karate has a larger ecosystem, more community resources, and built-in support for protocols (gRPC, WebSocket) not yet available as Azertio plugins.&lt;/p&gt;

&lt;h3&gt;
  
  
  vs. Postman / Newman
&lt;/h3&gt;

&lt;p&gt;Postman is an excellent tool for interactive API exploration. Newman makes collections runnable in CI. The problems emerge when teams graduate to treating tests as code: Postman collections are proprietary JSON that produces noisy diffs, assertions are JavaScript scattered across collection items, and there is no database testing, no benchmark mode, and no persistent execution history.&lt;/p&gt;

&lt;p&gt;Azertio's &lt;code&gt;.feature&lt;/code&gt; files are plain text, fully diffable, reviewable in pull requests, and readable by business stakeholders. The two tools are not mutually exclusive — many teams use Postman for initial exploration and translate validated scenarios into Azertio for the automated regression suite.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Download the distribution from GitHub releases and add bin/ to PATH&lt;/span&gt;

&lt;span class="c"&gt;# 2. Create azertio.yaml in your project&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; azertio.yaml &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
testProject:
  name: My Project
  test-suites:
    - name: smoke
      tag-expression: "smoke"
plugins:
  - gherkin
  - rest
configuration:
  rest:
    baseURL: "https://jsonplaceholder.typicode.com"
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# 3. Write a feature file&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; features &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; features/posts.feature &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
@smoke
Feature: Posts API

  Scenario: Retrieve a post
    When I make a GET request to "posts/1"
    Then the HTTP status code is equal to 200
    And the response body contains:
      """json
      { "id": 1 }
      """
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# 4. Install plugins&lt;/span&gt;
azertio &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# 5. Run&lt;/span&gt;
azertio run &lt;span class="nt"&gt;-s&lt;/span&gt; smoke
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Where Things Stand
&lt;/h2&gt;

&lt;p&gt;Azertio is in early alpha. The core runtime, the REST plugin, and the database plugin are functional; the VS Code extension is published. Protocol plugins for gRPC, GraphQL, and WebSocket are on the roadmap but not yet available.&lt;/p&gt;

&lt;p&gt;If you are building a new test suite for API and database testing and want clean feature files, no glue code, and execution history out of the box — it is worth a look.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/org-azertio/azertio" rel="noopener noreferrer"&gt;github.com/org-azertio/azertio&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Getting started:&lt;/strong&gt; &lt;a href="https://github.com/org-azertio/azertio/blob/main/docs/getting-started.md" rel="noopener noreferrer"&gt;docs/getting-started.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>api</category>
      <category>opensource</category>
      <category>showdev</category>
      <category>testing</category>
    </item>
    <item>
      <title>JExten: Building a Robust Plugin Architecture with Java Modules (JPMS)</title>
      <dc:creator>Luis Iñesta Gelabert</dc:creator>
      <pubDate>Thu, 15 Jan 2026 16:45:48 +0000</pubDate>
      <link>https://dev.to/luiinge/jexten-building-a-robust-plugin-architecture-with-java-modules-jpms-47l2</link>
      <guid>https://dev.to/luiinge/jexten-building-a-robust-plugin-architecture-with-java-modules-jpms-47l2</guid>
      <description>&lt;h2&gt;
  
  
  1. Motivation: The Road to Modular Isolation
&lt;/h2&gt;

&lt;p&gt;When building extensible applications in Java, developers often start with a simple question: "How can I let users add functionality without recompiling the core application?" The journey usually begins with the standard &lt;code&gt;java.util.ServiceLoader&lt;/code&gt;, which provides a simple mechanism for discovering implementations of an interface.&lt;/p&gt;

&lt;p&gt;However, as the application grows, a critical problem emerges: &lt;strong&gt;"Classpath Hell."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine you have a host application that uses &lt;code&gt;library-v1&lt;/code&gt;. You create a plugin system, and someone writes a "Twitter Plugin" that requires &lt;code&gt;library-v2&lt;/code&gt;. If you run everything on the same flat classpath, you get a conflict. Either the host crashes because it gets the wrong version of the library, or the plugin fails. You cannot have two versions of the same library on the classpath without facing the risk of runtime exceptions such as &lt;code&gt;ClassDefNotFoundError&lt;/code&gt; or &lt;code&gt;NoSuchMethodError&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This was the driving motivation behind &lt;strong&gt;JExten&lt;/strong&gt;. I needed a way to strictly encapsulate plugins so that each one could define its own dependencies without affecting the host or other plugins.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enter JPMS (Java Platform Module System)
&lt;/h3&gt;

&lt;p&gt;Java 9 introduced the Module System (JPMS), which provides strong encapsulation and explicit dependency graphs. It allows us to create isolated "layers" of modules.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Boot Layer&lt;/strong&gt;: The JVM and platform modules.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Host Layer&lt;/strong&gt;: The core application and its dependencies.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Plugin Layers&lt;/strong&gt;: Dynamically created layers on top of the host layer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By leveraging JPMS ModuleLayers, JExten allows Plugin A to rely on &lt;code&gt;Jackson 2.14&lt;/code&gt; while Plugin B relies on &lt;code&gt;Jackson 2.10&lt;/code&gt;, and both can coexist peacefully within the same running application.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Architecture and Design
&lt;/h2&gt;

&lt;p&gt;JExten is designed to be lightweight and annotation-driven, abstracting away the complexity of raw ModuleLayers while providing powerful features like Dependency Injection (DI) and lifecycle management.&lt;/p&gt;

&lt;p&gt;The architecture consists of three main pillars:&lt;/p&gt;

&lt;h3&gt;
  
  
  The Extension Model
&lt;/h3&gt;

&lt;p&gt;At the core, JExten uses a clean separation between the "contract" (API) and the "implementation".&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Extension Point (&lt;code&gt;@ExtensionPoint&lt;/code&gt;)&lt;/strong&gt;: An interface defined in the host application (or a shared API module) that defines what functionality can be extended.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@ExtensionPoint&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.0"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;PaymentGateway&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Extension (&lt;code&gt;@Extension&lt;/code&gt;)&lt;/strong&gt;: The concrete implementation provided by a plugin.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Extension&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Priority&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;HIGH&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StripeGateway&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;PaymentGateway&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Notice that you can make use of the ExtensionManager without the PluginManager. This is useful for testing or when you want to use JExten in a non-plugin environment and all the extensions are already available in the modulepath.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Manager Split
&lt;/h3&gt;

&lt;p&gt;To separate concerns, the library splits responsibilities into two distinct managers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;PluginManager ("The Physical Layer")&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  This component handles the raw artifacts (JARs/ZIPs).&lt;/li&gt;
&lt;li&gt;  It verifies integrity using SHA-256 checksums ensuring that plugins haven't been tampered with.&lt;/li&gt;
&lt;li&gt;  It builds the JPMS &lt;code&gt;ModuleLayer&lt;/code&gt; graph. It reads the &lt;code&gt;plugin.yaml&lt;/code&gt; manifest, resolves dependencies (from a local cache or Maven repo), and constructs the classloading environment.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ExtensionManager ("The Logical Layer")&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Once layers are built, this component takes over.&lt;/li&gt;
&lt;li&gt;  It scans the layers for classes annotated with &lt;code&gt;@Extension&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  It manages the lifecycle of these extensions (Singleton, Session, or Prototype scopes).&lt;/li&gt;
&lt;li&gt;  It handles &lt;strong&gt;Dependency Injection&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Dependency Injection (DI)
&lt;/h4&gt;

&lt;p&gt;Since plugins run in isolated layers, standard DI frameworks (like Spring or Guice) can sometimes be "too heavy" or tricky to configure across dynamic module boundaries. JExten includes a built-in, lightweight DI system.&lt;/p&gt;

&lt;p&gt;You can simply use &lt;code&gt;@Inject&lt;/code&gt; to wire extensions together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Extension&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyPluginService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Inject&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;PaymentGateway&lt;/span&gt; &lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Automatically injects the highest priority implementation&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works seamlessly across module boundaries. A plugin can inject a service provided by the host, or even a service provided by another plugin (if the module graph allows it).&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Usage Example
&lt;/h2&gt;

&lt;p&gt;Here is a quick look at how to define an extension point, implement it in a plugin, and use it in your application.&lt;/p&gt;

&lt;h3&gt;
  
  
  I. Define an Extension Point
&lt;/h3&gt;

&lt;p&gt;Create an interface and annotate it with &lt;code&gt;@ExtensionPoint&lt;/code&gt;. This is the contract that plugins will implement.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@ExtensionPoint&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.0"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;Greeter&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;greet&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  II. Implement an Extension
&lt;/h3&gt;

&lt;p&gt;In your plugin module, implement the interface and annotate it with &lt;code&gt;@Extension&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Extension&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FriendlyGreeter&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Greeter&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;greet&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Hello, "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"!"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  III. Discover and Use
&lt;/h3&gt;

&lt;p&gt;In your host application, use the &lt;code&gt;ExtensionManager&lt;/code&gt; to discover and invoke extensions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Main&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Initialize the manager&lt;/span&gt;
        &lt;span class="nc"&gt;ExtensionManager&lt;/span&gt; &lt;span class="n"&gt;manager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ExtensionManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pluginManager&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Get all extensions for the Greeter point&lt;/span&gt;
        &lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getExtensions&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Greeter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
               &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forEach&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;greeter&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;greeter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;greet&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"World"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  IV. Package your Extension(s) as a Plugin
&lt;/h3&gt;

&lt;p&gt;Finally, use the &lt;code&gt;jexten-maven-plugin&lt;/code&gt; Maven plugin to check your &lt;code&gt;module-info.java&lt;/code&gt; at compile time and package your extension into a ZIP bundle that includes all dependencies and the generated &lt;code&gt;plugin.yaml&lt;/code&gt; manifest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;plugin&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.myjtools.jexten&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;jexten-maven-plugin&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;1.0.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;executions&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;execution&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;goals&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;goal&amp;gt;&lt;/span&gt;generate-manifest&lt;span class="nt"&gt;&amp;lt;/goal&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;goal&amp;gt;&lt;/span&gt;assemble-bundle&lt;span class="nt"&gt;&amp;lt;/goal&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/goals&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/execution&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/executions&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;configuration&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;hostModule&amp;gt;&lt;/span&gt;com.example.app&lt;span class="nt"&gt;&amp;lt;/hostModule&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/configuration&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/plugin&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can then install the generated ZIP bundle to your host application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Application&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;IOException&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Path&lt;/span&gt; &lt;span class="n"&gt;pluginDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"plugins"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Create plugin manager&lt;/span&gt;
        &lt;span class="nc"&gt;PluginManager&lt;/span&gt; &lt;span class="n"&gt;pluginManager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PluginManager&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"org.myjtools.jexten.example.app"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// Application ID&lt;/span&gt;
            &lt;span class="nc"&gt;Application&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getClassLoader&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;pluginDir&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Install plugin from bundle&lt;/span&gt;
        &lt;span class="n"&gt;pluginManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;installPluginFromBundle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;pluginDir&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"my-plugin-1.0.0.zip"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Create extension manager with plugin support&lt;/span&gt;
        &lt;span class="nc"&gt;ExtensionManager&lt;/span&gt; &lt;span class="n"&gt;extensionManager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ExtensionManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pluginManager&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

         &lt;span class="c1"&gt;// Get extensions from the plugin&lt;/span&gt;
        &lt;span class="n"&gt;extensionManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getExtensions&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Greeter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forEach&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;greeter&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;greeter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;greet&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"World"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Comparison with Other Solutions
&lt;/h2&gt;

&lt;p&gt;Choosing the right plugin framework depends on your specific needs. Here is how JExten compares to established alternatives:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://pf4j.org/" rel="noopener noreferrer"&gt;PF4J (Plugin Framework for Java)&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;PF4J&lt;/strong&gt; is a mature, lightweight plugin framework that relies on &lt;strong&gt;ClassLoader isolation&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Isolation&lt;/strong&gt;: PF4J uses custom ClassLoaders to isolate plugins. JExten uses &lt;strong&gt;JPMS ModuleLayers&lt;/strong&gt;. The latter is the "native" Java way to handle isolation since Java 9, strictly enforcing encapsulation at the JVM level.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Modernity&lt;/strong&gt;: While PF4J is excellent, JExten is designed specifically for the modern modular Java ecosystem (Java 21+), taking advantage of module descriptors (&lt;code&gt;module-info.java&lt;/code&gt;) for defining dependencies rather than custom manifests.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://www.osgi.org/" rel="noopener noreferrer"&gt;OSGi&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;OSGi&lt;/strong&gt; is the gold standard for modularity, powering IDEs like Eclipse.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Complexity&lt;/strong&gt;: OSGi is powerful but comes with a steep learning curve and significant boilerplate (Manifest headers, Activators, complex service dynamics). JExten offers a fraction of the complexity ("OSGi Lite") by focusing on the 80% use case: strictly isolated extensions with simple dependency injection, without requiring a full OSGi container.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Runtime&lt;/strong&gt;: OSGi brings a heavy runtime. JExten is a lightweight library that sits on top of the standard JVM features.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://github.com/moditect/layrry" rel="noopener noreferrer"&gt;Layrry&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Layrry&lt;/strong&gt; is a launcher and API for executing modular Java applications.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Scope&lt;/strong&gt;: Layrry focuses heavily on the &lt;em&gt;configuration and assembly&lt;/em&gt; of module layers (often via YAML/TOML) and acts as a runner. JExten focuses on the &lt;em&gt;programming model&lt;/em&gt; within those layers.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Features&lt;/strong&gt;: Layrry is great for constructing the layers, but it doesn't provide an opinionated application framework. JExten provides the "glue" code—Extension Points, Dependency Injection, and Lifecycle Management—that you would otherwise have to write yourself when using raw Module Layers or Layrry.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;JExten&lt;/th&gt;
&lt;th&gt;PF4J&lt;/th&gt;
&lt;th&gt;OSGi&lt;/th&gt;
&lt;th&gt;Layrry&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Isolation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JPMS ModuleLayers&lt;/td&gt;
&lt;td&gt;File/ClassLoader&lt;/td&gt;
&lt;td&gt;Bundle ClassLoaders&lt;/td&gt;
&lt;td&gt;JPMS ModuleLayers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Configuration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Java Annotations&lt;/td&gt;
&lt;td&gt;Properties/Manifest&lt;/td&gt;
&lt;td&gt;Manifest Headers&lt;/td&gt;
&lt;td&gt;YAML/TOML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dependency Injection&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Built-in (&lt;code&gt;@Inject&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;External (Spring/Guice)&lt;/td&gt;
&lt;td&gt;Declarative Services&lt;/td&gt;
&lt;td&gt;None (ServiceLoader)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Learning Curve&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  5. Conclusion
&lt;/h2&gt;

&lt;p&gt;JExten is a lightweight, annotation-driven plugin framework that leverages JPMS ModuleLayers to provide isolation and dependency management. It is designed to be easy to use and understand, with a focus on simplicity and ease of use. &lt;/p&gt;

&lt;p&gt;Finally, keep in mind that JExten is still in its early stages, and there is much room for improvement. Feel free to contribute to the project on GitHub and/or engage in a discussion in the issues section. Link to the repository is &lt;a href="https://github.com/org-myjtools/jexten" rel="noopener noreferrer"&gt;here&lt;/a&gt; .&lt;/p&gt;

</description>
      <category>java</category>
      <category>opensource</category>
      <category>plugins</category>
      <category>jpms</category>
    </item>
  </channel>
</rss>
