<?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>What I underestimated about open-source: getting users after the project already works</title>
      <dc:creator>Luis Iñesta Gelabert</dc:creator>
      <pubDate>Wed, 03 Jun 2026 15:10:42 +0000</pubDate>
      <link>https://dev.to/luiinge/what-i-underestimated-about-open-source-getting-users-after-the-project-already-works-54ia</link>
      <guid>https://dev.to/luiinge/what-i-underestimated-about-open-source-getting-users-after-the-project-already-works-54ia</guid>
      <description>&lt;p&gt;I’m currently building an open-source project, and I think I understand fairly well why getting users is hard — but I still don’t have a clear mental model for what actually moves the needle.&lt;br&gt;
I already wrote about &lt;a href="https://dev.to/luiinge/azertio-api-and-database-testing-without-the-glue-code-1ibl"&gt;Azertio&lt;/a&gt; itself in a previous post, so I won’t go into what it is or how it works in detail.&lt;/p&gt;

&lt;p&gt;This is more about something I’ve learned while building it: the gap between “a working open-source project” and “a project that actually gets users” is much larger than I expected.&lt;/p&gt;

&lt;p&gt;I think I understand the reasons behind it fairly well at this point, but I still don’t have a clear model for what actually turns that understanding into traction.&lt;/p&gt;

&lt;p&gt;A few observations from the experience so far:&lt;/p&gt;

&lt;h2&gt;
  
  
  “It works” is not a growth factor
&lt;/h2&gt;

&lt;p&gt;Once a project reaches a usable baseline, functionality stops being the main constraint.&lt;/p&gt;

&lt;p&gt;At that point, additional improvements tend to have diminishing returns in terms of adoption. The difference between “good enough” and “very good” is often not what determines whether people try it. Most potential users never reach that evaluation stage in the first place.&lt;/p&gt;

&lt;p&gt;In practice, “it works” is necessary, but almost irrelevant for getting initial attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discovery is the real bottleneck
&lt;/h2&gt;

&lt;p&gt;The hardest problem is not convincing someone who has already seen the project — it’s getting them to see it at all.&lt;/p&gt;

&lt;p&gt;There are a few concentrated channels where discovery happens (communities, feeds, word of mouth), and if you don’t get initial visibility in those places, the project tends to remain invisible regardless of quality.&lt;/p&gt;

&lt;p&gt;This makes early distribution disproportionately important compared to everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  OSS removes friction of use, not friction of adoption
&lt;/h2&gt;

&lt;p&gt;Open source is often associated with easier adoption, but that only applies after someone is already interested.&lt;/p&gt;

&lt;p&gt;It reduces barriers like pricing, licensing, and sometimes trust. But it doesn’t meaningfully reduce the biggest barrier, which is awareness.&lt;/p&gt;

&lt;p&gt;In other words, OSS helps once the decision to try it is already made — but does very little to influence whether that decision happens at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Early momentum dominates everything
&lt;/h2&gt;

&lt;p&gt;There seems to be a strong feedback loop at the beginning of a project’s life.&lt;/p&gt;

&lt;p&gt;If early exposure happens in the right places, momentum can build relatively quickly. If it doesn’t, the project tends to plateau, even if it is useful or well designed.Later improvements don’t seem to fully compensate for missing that initial wave of attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I missing
&lt;/h2&gt;

&lt;p&gt;What I understand reasonably well is why this happens. The part I don’t have is a clear sense of what actually works as a lever at that early stage. I can list the usual approaches — writing about it, posting in communities, word of mouth, waiting for timing, etc. — but I don’t yet have a strong mental model for what reliably creates that first meaningful set of users in practice.&lt;/p&gt;

&lt;p&gt;I’d be interested in hearing from others. If you’ve worked on open-source projects, I’d really like to hear your experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What actually brought your first meaningful users to a project?&lt;/li&gt;
&lt;li&gt;Was there a specific channel or action that made the difference, or was it mostly timing and persistence?&lt;/li&gt;
&lt;li&gt;Looking back, what do you think you underestimated most at the beginning?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’m less interested in generic advice and more in concrete experiences or patterns you’ve personally seen work (or fail). I’m trying to understand what the real “levers” are at this stage, if they exist at all.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>discuss</category>
    </item>
    <item>
      <title>When Cucumber Grows Too Big: Pain Points, Lessons Learned, and Alternatives</title>
      <dc:creator>Luis Iñesta Gelabert</dc:creator>
      <pubDate>Wed, 27 May 2026 13:19:13 +0000</pubDate>
      <link>https://dev.to/luiinge/when-cucumber-grows-too-big-pain-points-lessons-learned-and-alternatives-21pm</link>
      <guid>https://dev.to/luiinge/when-cucumber-grows-too-big-pain-points-lessons-learned-and-alternatives-21pm</guid>
      <description>&lt;h2&gt;
  
  
  What Is Cucumber, and Why Do Teams Love It?
&lt;/h2&gt;

&lt;p&gt;If you have spent any time in behavior-driven development (BDD), you have almost certainly encountered Cucumber. First released in 2008, it has become the de facto standard for writing executable specifications in plain language.&lt;/p&gt;

&lt;p&gt;The core idea is elegant: tests are written in &lt;strong&gt;Gherkin&lt;/strong&gt;, a structured natural language format built around three keywords — &lt;code&gt;Given&lt;/code&gt;, &lt;code&gt;When&lt;/code&gt;, and &lt;code&gt;Then&lt;/code&gt;. A product owner, a tester, and a developer can all read the same file and agree on what the system is supposed to do.&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="kd"&gt;Feature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; User login

  &lt;span class="kn"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; Successful login with valid credentials
    &lt;span class="nf"&gt;Given &lt;/span&gt;the user &lt;span class="s"&gt;"alice"&lt;/span&gt; exists with password &lt;span class="s"&gt;"secret"&lt;/span&gt;
    &lt;span class="nf"&gt;When &lt;/span&gt;she submits the login form
    &lt;span class="nf"&gt;Then &lt;/span&gt;she is redirected to the dashboard
    &lt;span class="nf"&gt;And &lt;/span&gt;a session cookie is set
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood, each step is matched by a regular expression or a cucumber expression to a &lt;strong&gt;step definition&lt;/strong&gt; — a method in Java, Ruby, JavaScript, or whatever language your project uses. Cucumber finds the right method, runs it, and aggregates the results into a report.&lt;/p&gt;

&lt;p&gt;The promise is compelling: &lt;strong&gt;business-readable tests that are also executable&lt;/strong&gt;. The gap between what stakeholders describe and what testers automate, closed forever. In small projects and well-contained modules, it genuinely works.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Things Start to Break Down
&lt;/h2&gt;

&lt;p&gt;I spent several years working on large backend systems where Cucumber was adopted as the standard integration testing tool. Early on, things were manageable. Over time, a set of recurring problems emerged that no amount of team discipline could fully solve.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Glue Code Explosion
&lt;/h3&gt;

&lt;p&gt;Every Gherkin step needs a step definition. In a project with hundreds of scenarios covering REST APIs, databases, message queues, and background jobs, this means hundreds — sometimes thousands — of step definition methods spread across dozens of classes.&lt;/p&gt;

&lt;p&gt;The immediate problem is &lt;strong&gt;discoverability&lt;/strong&gt;. When a new developer writes a step, how do they know whether it already exists? The IDE can sometimes help, but step matching relies on regular expressions that are not always obvious to navigate. You end up with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Duplicate step definitions that do subtly different things&lt;/li&gt;
&lt;li&gt;Slightly different phrasing that bypasses an existing step and creates a new one&lt;/li&gt;
&lt;li&gt;Inconsistent abstractions because different people solved the same problem independently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The deeper problem is &lt;strong&gt;coupling&lt;/strong&gt;. Step definitions are not unit-tested; they are integration plumbing. When a REST client is refactored, you find that fifteen step definitions directly instantiate it. When a database fixture format changes, you discover that nobody documented which step methods touch which tables.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Shared State and Context Passing
&lt;/h3&gt;

&lt;p&gt;Cucumber scenarios are supposed to be independent, but step definitions need to share state: the HTTP response from the &lt;code&gt;When&lt;/code&gt; step needs to be inspectable in the &lt;code&gt;Then&lt;/code&gt; step. The standard solution is a &lt;strong&gt;World object&lt;/strong&gt; (or &lt;code&gt;@ScenarioScoped&lt;/code&gt; beans in Java) — a bag of shared state injected into step definition classes.&lt;/p&gt;

&lt;p&gt;This works until you have fifteen step definition classes all mutating the same World, nobody owns the contract for what each field means, and a flaky test appears because some scenario left dirty state that wasn't cleaned up. Debugging it means reading glue code, not feature files — which defeats half the purpose of BDD.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The Feature File Drift Problem
&lt;/h3&gt;

&lt;p&gt;In a healthy BDD process, feature files are living documents co-authored by business and technical people. In practice, after the initial sprint, product owners stop reading them. They become developer-only artifacts, written with the same mindset as JUnit tests: exhaustive, technical, and opaque to anyone outside the team.&lt;/p&gt;

&lt;p&gt;You end up with scenarios like:&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="nf"&gt;Given &lt;/span&gt;the user entity with id 42 exists in schema &lt;span class="s"&gt;"core"&lt;/span&gt; table &lt;span class="s"&gt;"users"&lt;/span&gt; with status &lt;span class="s"&gt;"ACTIVE"&lt;/span&gt;
&lt;span class="nf"&gt;When &lt;/span&gt;the endpoint POST /api/v2/auth/session is called with payload from fixture &lt;span class="s"&gt;"auth_fixtures/alice_valid.json"&lt;/span&gt;
&lt;span class="nf"&gt;Then &lt;/span&gt;the response body path &lt;span class="s"&gt;"$.data.token"&lt;/span&gt; matches regex &lt;span class="s"&gt;"[A-Za-z0-9-_]{40}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not BDD. It is JUnit with extra ceremony.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Step Definition Scope Creep
&lt;/h3&gt;

&lt;p&gt;In Cucumber, step definitions are global. There is no namespacing, no module boundary, no way to say "these steps belong to the payments domain." As the test suite grows, you inherit every step ever written, and step expressions start colliding.&lt;/p&gt;

&lt;p&gt;Teams work around this with naming conventions, careful phrasing, and tribal knowledge. That is technical debt in disguise.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Maintenance Cost Compounds Over Time
&lt;/h3&gt;

&lt;p&gt;Every refactor of production code ripples through glue code. A renamed endpoint, a changed response schema, a migrated database table — each one can silently break dozens of step definitions, or worse, fail to break them because a step is now asserting against stale data that happens to still match.&lt;/p&gt;

&lt;p&gt;The test suite that was supposed to give you confidence becomes a maintenance burden that slows releases down. At some point the question stops being "how do we fix the tests?" and becomes "is this the right tool for this job?"&lt;/p&gt;




&lt;h2&gt;
  
  
  The Specific Pain That Made Me Reconsider
&lt;/h2&gt;

&lt;p&gt;The breaking point for me was the combination of two things happening simultaneously.&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;onboarding friction&lt;/strong&gt;. A new team member joining the project needed days to understand the glue code before they could write a single new test. The feature files were not self-explanatory; they were a surface sitting on top of an iceberg of implementation. That is the opposite of what BDD promises.&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;the semantic gap for API testing&lt;/strong&gt;. Our integration tests were almost entirely black-box: send an HTTP request, assert on the response, check the database state. For this use case, Cucumber adds a translation layer — Gherkin step → step definition → HTTP client call — that provides no value. The "business readable" framing makes no sense for &lt;code&gt;Then the response status is 200&lt;/code&gt;. Nobody is showing those files to a product owner.&lt;/p&gt;

&lt;p&gt;We were paying the full cost of Cucumber's glue code model while getting almost none of its BDD benefits.&lt;/p&gt;




&lt;h2&gt;
  
  
  Alternatives Worth Considering
&lt;/h2&gt;

&lt;p&gt;Depending on what is actually causing your pain, different tools address different problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://karatelabs.github.io/karate/" rel="noopener noreferrer"&gt;Karate&lt;/a&gt;&lt;/strong&gt; is the closest direct alternative for API testing. It uses a Gherkin-like syntax but eliminates step definitions entirely — steps are interpreted directly by the framework. You get zero glue code for REST and GraphQL testing, plus built-in mocking and performance testing. If your Cucumber usage is primarily API testing, Karate is worth a serious look.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://rest-assured.io/" rel="noopener noreferrer"&gt;REST-assured&lt;/a&gt;&lt;/strong&gt; (with JUnit or TestNG) takes the opposite position: abandon the DSL entirely and write your tests as code. You lose the business-readable layer, but you gain the full power of a proper programming language — real abstractions, composable helpers, IDE support, type safety. For teams that have already given up on non-developer readers, this is often the pragmatic choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://playwright.dev/" rel="noopener noreferrer"&gt;Playwright&lt;/a&gt; / &lt;a href="https://www.cypress.io/" rel="noopener noreferrer"&gt;Cypress&lt;/a&gt;&lt;/strong&gt; are not Cucumber replacements, but if your integration tests are UI-heavy, their built-in test organization and recording capabilities may do more for you than Cucumber's BDD layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://specflow.org/" rel="noopener noreferrer"&gt;SpecFlow&lt;/a&gt;&lt;/strong&gt; (for .NET) and &lt;strong&gt;&lt;a href="https://behave.readthedocs.io/" rel="noopener noreferrer"&gt;Behave&lt;/a&gt;&lt;/strong&gt; (for Python) are Cucumber-family tools that sometimes have better ecosystem integration for their respective stacks, though they share the same architectural tradeoffs.&lt;/p&gt;

&lt;p&gt;The rule of thumb I arrived at: &lt;strong&gt;Cucumber earns its keep when the scenario files are genuinely read and validated by non-developers on a regular basis.&lt;/strong&gt; If that is not happening — and it often is not — you are paying glue-code tax for a benefit you are not receiving.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Tool Built From These Lessons
&lt;/h2&gt;

&lt;p&gt;After working through these problems repeatedly, I designed &lt;strong&gt;&lt;a href="http://azertio.org" rel="noopener noreferrer"&gt;Azertio&lt;/a&gt;&lt;/strong&gt; as an attempt to take what Cucumber gets right (human-readable Gherkin, structured scenario files) and eliminate what causes the most pain in large projects.&lt;/p&gt;

&lt;p&gt;The central bet: for black-box testing of REST APIs and databases, &lt;strong&gt;there should be no glue code at all&lt;/strong&gt;. Steps are provided by plugins loaded at runtime — &lt;code&gt;rest&lt;/code&gt;, &lt;code&gt;db&lt;/code&gt;, and others — and every step in those plugins is immediately available in any feature file without any wiring. You declare which plugins you use in a YAML config file and write tests immediately.&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 an order reduces stock
  &lt;span class="err"&gt;Given db table stock has&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;sku&lt;/span&gt;   &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;units&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;P-001&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;10&lt;/span&gt;    &lt;span class="p"&gt;|&lt;/span&gt;
  &lt;span class="err"&gt;When I make a POST request to "orders" with body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;"""json
    { "sku": "P-001", "quantity": 3 }
    """&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;db table stock row where sku = &lt;span class="s"&gt;"P-001"&lt;/span&gt; has units = 7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No step definitions. No World objects. No regex to maintain.&lt;/p&gt;

&lt;p&gt;It also directly addresses the definition/implementation split that Cucumber struggles with: you can write a &lt;strong&gt;definition&lt;/strong&gt; feature (business-readable, owned by the product team) and a separate &lt;strong&gt;implementation&lt;/strong&gt; feature (technical, owned by testers), linked by a tag. The execution report shows the business structure; the implementation details stay out of the way.&lt;/p&gt;

&lt;p&gt;The project is open source, still early, and genuinely shaped by the frustrations described in this article. If any of this resonates with your own Cucumber experience, I would be glad to hear your feedback at &lt;a href="http://azertio.org" rel="noopener noreferrer"&gt;azertio.org&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have you hit any of these Cucumber pain points in your own projects? Which solutions worked for you? Let me know in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cucumber</category>
      <category>testing</category>
      <category>bdd</category>
      <category>azertio</category>
    </item>
    <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>
