eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Course – Summer Sale 2026 – NPI EA (cat= Baeldung)
announcement - icon

Yes, we're now running our only Summer Sale. All Courses are 30% off until 20th July, 2026:

>> EXPLORE ACCESS NOW

Course – Summer Sale 2026 – NPI (cat=Baeldung)
announcement - icon

Yes, we're now running our only Summer Sale. All Courses are 30% off until 20th July, 2026:

>> EXPLORE ACCESS NOW

1. Overview

Quarkus is a framework composed of a core and a set of extensions. The core is based on Context and Dependency Injection (CDI) and extensions are usually meant to integrate a third-party framework by exposing their primary components as CDI beans.

In this tutorial, we’ll focus on how to write a Quarkus extension assuming a basic understanding of Quarkus.

2. What’s a Quakus Extension

A Quarkus extension is simply a module that can run on top of a Quarkus application. The Quarkus application itself is a core module with a set of other extensions.

The most common use case for such an extension is to get a third-party framework running on top of a Quarkus application.

3. Running Liquibase in a Plain Java Application

Let’s try and implement an extension for integrating Liquibase, a tool for database change management.

But before we dive in, we first need to show how to run a Liquibase migration from a Java main method. This will hugely facilitate implementing the extension.

The entry point for the Liquibase framework is the Liquibase API. To use this, we need a changelog file, a ClassLoader for accessing this file, and a Connection to the underlying database:

Connection c = DriverManager.getConnection("jdbc:h2:mem:testdb", "user", "password");
ResourceAccessor resourceAccessor = new ClassLoaderResourceAccessor();
String changLogFile = "db/liquibase-changelog-master.xml";
Liquibase liquibase = new Liquibase(changLogFile, resourceAccessor, new JdbcConnection(c));

Having this instance, we simply call the update() method which updates the database to match the changelog file.

liquibase.update(new Contexts());

The goal is to expose Liquibase as a Quarkus extension. That is, providing a database configuration and changelog file through Quarkus Configuration and then producing the Liquibase API as a CDI bean. This provides a means for recording migration invocation for later execution.

4. How to Write a Quarkus Extension

Technically speaking, a Quarkus extension is a Maven multi-module project composed of two modules. The first is a runtime module where we implement requirements. The second is a deployment module for processing configuration and generating the runtime code.

So, let’s start by creating a Maven multi-module project called quarkus-liquibase-parent that contains two submodules, runtime and deployment:

<modules>
    <module>runtime</module>
    <module>deployment</module>
</modules>

5. Implementing the Runtime Module

In the runtime module, we’ll implement:

  • a configuration class for capturing the Liquibase changelog file
  • a CDI producer for exposing the Liquibase API
  • and a recorder that acts as a proxy for recording invocation calls

5.1. Maven Dependencies and Plugins

The runtime module will depend on the quarkus-core module and eventually the runtime modules of the needed extensions. Here, we need the quarkus-agroal dependency as our extension needs a Datasource. We’ll include the Liquibase library here, too:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-core</artifactId>
    <version>${quarkus.version}</version>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-agroal</artifactId>
    <version>${quarkus.version}</version>
</dependency>
<dependency>
    <groupId>org.liquibase</groupId>
    <artifactId>liquibase-core</artifactId>
    <version>4.26.0</version>
</dependency>

Also, we may need to add the quarkus-bootstrap-maven-plugin. This plugin automatically generates the Quarkus extension descriptor by calling the extension-descriptor goal.

Or, we can omit this plugin and generate the descriptor manually.

Either way, we can find the extension descriptor is located under META-INF/quarkus-extension.properties:

deployment-artifact=com.baeldung.quarkus.liquibase\:deployment\:1.0-SNAPSHOT

5.2. Exposing the Configuration

To provide the changelog file, we need to implement a configuration class:

@ConfigRoot(name = "liquibase", phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED)
public final class LiquibaseConfig {
    @ConfigItem
    public String changeLog;
}

We annotate the class by @ConfigRoot and the properties by @ConfigItem. So, the changeLog field, which is the camel case form of the change-log, will be provided through the quarkus.liquibase.change-log key in the application.properties file, located in a Quarkus application classpath:

quarkus.liquibase.change-log=db/liquibase-changelog-master.xml

We can also note the ConfigRoot.phase value which instructs when to resolve the change-log key. In this case, BUILD_AND_RUN_TIME_FIXED, the key is read at deployment time and available to the application at runtime.

5.3. Exposing the Liquibase API as a CDI Bean

We’ve seen above how to run a Liquibase migration from the main method.

Now, we’ll reproduce the same code but as a CDI bean, and we’ll use a CDI producer for that purpose:

@Produces
public Liquibase produceLiquibase() throws Exception {
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    ResourceAccessor resourceAccessor = new ClassLoaderResourceAccessor(classLoader);
    DatabaseConnection jdbcConnection = new JdbcConnection(dataSource.getConnection());
    Liquibase liquibase = new Liquibase(liquibaseConfig.changeLog, resourceAccessor, jdbcConnection);
    return liquibase;
}

5.4. Recording Bytecode

In this step, we’ll write a recorder class that acts as a proxy for recording bytecode and setting up the runtime logic:

@Recorder
public class LiquibaseRecorder {

    public BeanContainerListener setLiquibaseConfig(LiquibaseConfig liquibaseConfig) {
        return beanContainer -> {
            LiquibaseProducer producer = beanContainer.instance(LiquibaseProducer.class);
            producer.setLiquibaseConfig(liquibaseConfig);
        };
    }

    public void migrate(BeanContainer container) throws LiquibaseException {
        Liquibase liquibase = container.instance(Liquibase.class);
        liquibase.update(new Contexts());
    }

}

Here, we have to record two invocations. setLiquibaseConfig for setting configuration and migrate for executing the migration. Next, we’ll look at how these recorder methods are called by the deployment build step processors which we’ll implement in the deployment module.

Note that when we invoke these recorder methods at build time, instructions are not executed but recorded for later execution at startup time.

6. Implementing the Deployment Module

The central components in a Quarkus extension are the Build Step Processors. They are methods annotated as @BuildStep that generate bytecode through recorders, and they are executed during the build time through the build goal of the quarkus-maven-plugin configured in a Quarkus application.

@BuildSteps are ordered thanks to BuildItems. They consume build items generated by early build steps and produce build items for later build steps.

The generated code by all the ordered build steps found in the application deployment modules is actually the runtime code.

6.1. Maven Dependencies

The deployment module should depend on the corresponding runtime module and eventually on the deployment modules of the needed extensions:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-core-deployment</artifactId>
    <version>${quarkus.version}</version>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-arc-deployment</artifactId>
    <version>${quarkus.version}</version>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-agroal-deployment</artifactId>
    <version>${quarkus.version}</version>
</dependency>

<dependency>
    <groupId>com.baeldung.quarkus.liquibase</groupId>
    <artifactId>runtime</artifactId>
    <version>${project.version}</version>
</dependency>

The latest stable version of Quarkus extensions is the same for the runtime module.

6.2. Implementing Build Step Processors

Now, let’s implement two build step processors for recording bytecode. The first build step processor is the build() method which will record bytecode for execution in the static init method. We configure this through the STATIC_INIT value:

@Record(ExecutionTime.STATIC_INIT)
@BuildStep
void build(BuildProducer<AdditionalBeanBuildItem> additionalBeanProducer,
  BuildProducer<FeatureBuildItem> featureProducer,
  LiquibaseRecorder recorder,
  BuildProducer<BeanContainerListenerBuildItem> containerListenerProducer,
  DataSourceInitializedBuildItem dataSourceInitializedBuildItem) {

    featureProducer.produce(new FeatureBuildItem("liquibase"));

    AdditionalBeanBuildItem beanBuilItem = AdditionalBeanBuildItem.unremovableOf(LiquibaseProducer.class);
    additionalBeanProducer.produce(beanBuilItem);

    containerListenerProducer.produce(
      new BeanContainerListenerBuildItem(recorder.setLiquibaseConfig(liquibaseConfig)));
}

First, we create a FeatureBuildItem to mark the type or the name of the extension. Then, we create an AdditionalBeanBuildItem so that the LiquibaseProducer bean will be available for the Quarkus container.

Finally, we create a BeanContainerListenerBuildItem in order to fire the BeanContainerListener after the Quarkus BeanContainer startup. Here, in the listener, we pass the configuration to the Liquibase bean.

The processMigration(), in turn, will record the invocation for execution in the main method as it’s configured using the RUNTIME_INIT parameter for recording.

@Record(ExecutionTime.RUNTIME_INIT)
@BuildStep
void processMigration(LiquibaseRecorder recorder, 
  BeanContainerBuildItem beanContainer) throws LiquibaseException {
    recorder.migrate(beanContainer.getValue());
}

Here, in this processor, we just called the migrate() recorder method, which in turn records the update() Liquibase method for later execution.

7. Testing the Liquibase Extension

To test our extension, we’ll first start by creating a Quarkus application using the quarkus-maven-plugin:

mvn io.quarkus:quarkus-maven-plugin:1.0.0.CR1:create\
-DprojectGroupId=com.baeldung.quarkus.app\
-DprojectArtifactId=quarkus-app

Next, we’ll add our extension as a dependency in addition to the Quarkus JDBC extension corresponding to our underlying database:

<dependency>
    <groupId>com.baeldung.quarkus.liquibase</groupId>
    <artifactId>runtime</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-jdbc-h2</artifactId>
    <version>1.0.0.CR1</version>
</dependency>

Next, we’ll need to have the quarkus-maven-plugin in our pom file:

<plugin>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-maven-plugin</artifactId>
    <version>${quarkus.version}</version>
    <executions>
        <execution>
            <goals>
                <goal>build</goal>
            </goals>
        </execution>
    </executions>
</plugin>

This is especially useful for running the application using the dev goal or building an executable using the build goal.

And next, we’ll provide data source configuration through the application.properties file located in src/main/resources:

quarkus.datasource.url=jdbc:h2:mem:testdb
quarkus.datasource.driver=org.h2.Driver
quarkus.datasource.username=user
quarkus.datasource.password=password

Next, we’ll provide the changelog configuration for our changelog file:

quarkus.liquibase.change-log=db/liquibase-changelog-master.xml

Finally, we can start the application either in dev mode:

mvn compile quarkus:dev

Or in production mode:

mvn clean package
java -jar target/quarkus-app-1.0-SNAPSHOT-runner.jar

8. Conclusion

In this article, we implemented a Quarkus extension. As an example, we have showcased how to get Liquibase running on top of a Quarkus application.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.
Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

Course – Summer Sale 2026 – NPI EA (cat= Baeldung)
announcement - icon

Yes, we're now running our only Summer Sale. All Courses are 30% off until 20th July, 2026:

>> EXPLORE ACCESS NOW

Course – Summer Sale 2026 – NPI (All)
announcement - icon

Yes, we're now running our only Summer Sale. All Courses are 30% off until 20th July, 2026:

>> EXPLORE ACCESS NOW

eBook Jackson – NPI EA – 3 (cat = Jackson)