Se afișează postările cu eticheta REST. Afișați toate postările
Se afișează postările cu eticheta REST. Afișați toate postările

7/30/2026

Database Persistence and Flyway in Quarkus

Database Persistence and Flyway in Quarkus 


Building a Production-Ready Product Management System with PostgreSQL



Introduction

In modern application development, managing database persistence and schema evolution are critical challenges. This comprehensive guide walks you through building a production-ready REST API using Quarkus, PostgreSQL, and Flyway for database migrations.

We'll build a Product Management System with a hierarchical warehouse positioning structure—a real-world scenario that demonstrates:

  • ⚡ Efficient database connection pooling with Agroal
  • 💾 Object-relational mapping with Hibernate and Panache
  • 🔄 Version-controlled schema migrations with Flyway
  • 🌳 Complex tree structures for warehouse management
  • 🚀 RESTful CRUD operations with proper transaction handling
  • 📊 Production-ready monitoring and health checks

By the end of this article, you'll have a complete understanding of how to configure, develop, and deploy a Quarkus application with robust database persistence.


Table of Contents

  1. Technology Stack Overview
  2. Project Setup and Dependencies
  3. Datasource Configuration
  4. Hibernate ORM and Panache
  5. Flyway Database Migrations
  6. Domain Model Design
  7. RESTful API Implementation
  8. Environment Configuration
  9. Testing and Monitoring
  10. Production Deployment
  11. Best Practices and Optimization
  12. Conclusion

1. Technology Stack Overview

Our application leverages the following technologies:

Component Technology Purpose
Framework Quarkus 3.x Cloud-native, supersonic subatomic Java
Database PostgreSQL 15 Open-source relational database
Connection Pool Agroal High-performance JDBC pooling
ORM Hibernate 6.x Object-relational mapping
Data Access Panache Active Record pattern for Hibernate
Migration Flyway Database version control
REST JAX-RS (RESTEasy) RESTful web services
JSON Jackson JSON serialization/deserialization

Why Quarkus?

Quarkus is designed for cloud-native and serverless applications, offering:

  • Fast startup time: < 1 second (native mode)
  • Low memory footprint: ~30MB RSS
  • Developer joy: Live reload, unified configuration
  • Container-first: Optimized for Docker and Kubernetes

Why PostgreSQL?

PostgreSQL provides enterprise-grade features:

  • Advanced SQL support with CTE (Common Table Expressions)
  • Native JSON/JSONB support
  • Excellent performance and reliability
  • Strong ACID compliance

2. Project Setup and Dependencies

Creating the Project

Start by creating a new Quarkus project using Maven:

mvn io.quarkus:quarkus-maven-plugin:3.6.0:create \\
    -DprojectGroupId=com.bmw.product \\
    -DprojectArtifactId=product-management-system \\
    -DclassName="com.bmw.product.resource.ProductResource" \\
    -Dpath="/api/products"

Maven Dependencies

Add the following dependencies to your pom.xml:

<dependencies>
    <!-- PostgreSQL JDBC Driver + Datasource -->
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-jdbc-postgresql</artifactId>
    </dependency>
    
    <!-- Hibernate ORM with Panache -->
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-hibernate-orm-panache</artifactId>
    </dependency>
    
    <!-- Flyway Database Migration -->
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-flyway</artifactId>
    </dependency>
    
    <!-- RESTEasy Reactive with Jackson -->
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-resteasy-reactive-jackson</artifactId>
    </dependency>
    
    <!-- Bean Validation -->
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-hibernate-validator</artifactId>
    </dependency>
    
    <!-- Health Checks -->
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-smallrye-health</artifactId>
    </dependency>
</dependencies>

Project Structure

Organize your project as follows:

src/
├── main/
│   ├── java/
│   │   └── com/bmw/product/
│   │       ├── model/
│   │       │   ├── Product.java
│   │       │   └── Position.java
│   │       ├── resource/
│   │       │   ├── ProductResource.java
│   │       │   └── PositionResource.java
│   │       └── health/
│   │           └── DatabaseHealthCheck.java
│   └── resources/
│       ├── application.properties
│       └── db/
│           └── migration/
│               ├── V1.0.0__create_positions_table.sql
│               ├── V1.0.1__create_products_table.sql
│               └── V1.0.2__insert_initial_data.sql
└── test/
    └── java/
        └── com/bmw/product/
            ├── ProductResourceTest.java
            └── PositionResourceTest.java

3. Datasource Configuration

The datasource is the foundation of database connectivity. Quarkus uses the Agroal connection pool, which offers excellent performance and native compilation support.

Basic Configuration

Add the following to src/main/resources/application.properties:

# ==========================================
# DATASOURCE CONFIGURATION
# ==========================================

# Database Type
quarkus.datasource.db-kind=postgresql

# Credentials
quarkus.datasource.username=postgres
quarkus.datasource.password=secret

# JDBC URL
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/productdb

# JDBC Driver (auto-detected, but can be explicit)
quarkus.datasource.jdbc.driver=org.postgresql.Driver

What happens here:

  1. Quarkus detects PostgreSQL as the database type
  2. Automatically loads the PostgreSQL JDBC driver
  3. Configures the Agroal connection pool
  4. Validates the connection on startup

Connection Pool Configuration

Optimize connection pooling for production:

# ==========================================
# AGROAL CONNECTION POOL SETTINGS
# ==========================================

# Pool Size Configuration
quarkus.datasource.jdbc.min-size=5
quarkus.datasource.jdbc.max-size=20
quarkus.datasource.jdbc.initial-size=5

# Timeout Settings
quarkus.datasource.jdbc.acquisition-timeout=10
quarkus.datasource.jdbc.background-validation-interval=2M
quarkus.datasource.jdbc.idle-removal-interval=5M
quarkus.datasource.jdbc.max-lifetime=30M

# Connection Validation
quarkus.datasource.jdbc.validation-query-sql=SELECT 1
quarkus.datasource.jdbc.detect-statement-leaks=true
quarkus.datasource.jdbc.leak-detection-interval=5M

# Transaction Isolation Level
quarkus.datasource.jdbc.transaction-isolation-level=read-committed

Key configuration parameters:

  • min-size/max-size: Balance between resource usage and performance. Formula: (core_count * 2) + effective_spindle_count
  • acquisition-timeout: How long to wait for a connection (in seconds)
  • validation-query-sql: Ensures connections are alive before use
  • leak-detection: Identifies unclosed connections (critical for debugging)

Health Checks and Metrics

Enable datasource monitoring:

# Health Check Configuration
quarkus.datasource.health.enabled=true

# Metrics Configuration
quarkus.datasource.metrics.enabled=true

This enables:

  • Health endpoint: http://localhost:8080/q/health
  • Metrics endpoint: http://localhost:8080/q/metrics

SSL/TLS Configuration (Production)

For production environments, enable SSL:

quarkus.datasource.jdbc.url=jdbc:postgresql://prod-db.bmw.com:5432/productdb?\\
  ssl=true&\\
  sslmode=verify-full&\\
  sslrootcert=/path/to/ca.crt

4. Hibernate ORM and Panache

Hibernate ORM handles the object-relational mapping, while Panache provides a simplified, active record-style API.

Hibernate Configuration

# ==========================================
# HIBERNATE ORM CONFIGURATION
# ==========================================

# Database Generation Strategy
# CRITICAL: Set to 'none' when using Flyway
quarkus.hibernate-orm.database.generation=none

# SQL Logging (Development)
quarkus.hibernate-orm.log.sql=true
quarkus.hibernate-orm.log.format-sql=true
quarkus.hibernate-orm.log.bind-parameters=true

# Disable Default Import Script
quarkus.hibernate-orm.sql-load-script=no-file

# Dialect (auto-detected)
quarkus.hibernate-orm.dialect=org.hibernate.dialect.PostgreSQLDialect

# Statistics (for monitoring)
quarkus.hibernate-orm.statistics=true

Important: Setting database.generation=none is critical when using Flyway. This prevents Hibernate from automatically creating/updating schema, which would conflict with Flyway's versioned migrations.

Why Panache?

Panache simplifies Hibernate usage by:

  • Eliminating boilerplate repository code
  • Providing an intuitive API
  • Supporting both Active Record and Repository patterns
  • Offering type-safe queries

Without Panache:

@Entity
public class Product {
    @Id @GeneratedValue
    private Long id;
    private String name;
    // getters, setters...
}

@ApplicationScoped
public class ProductRepository {
    @Inject EntityManager em;
    
    public List<Product> listAll() {
        return em.createQuery("SELECT p FROM Product p", Product.class)
                 .getResultList();
    }
}

With Panache:

@Entity
public class Product extends PanacheEntity {
    public String name;
    // No getters/setters needed for public fields
}

// Usage: Product.listAll()

5. Flyway Database Migrations

Flyway enables version-controlled, repeatable database migrations—essential for production applications.

Flyway Configuration

# ==========================================
# FLYWAY CONFIGURATION
# ==========================================

# Migration Behavior
quarkus.flyway.migrate-at-start=true
quarkus.flyway.baseline-on-migrate=true
quarkus.flyway.validate-on-migrate=true
quarkus.flyway.clean-at-start=false

# Baseline Configuration
quarkus.flyway.baseline-version=1.0.0
quarkus.flyway.baseline-description=Initial baseline

# Migration File Locations
quarkus.flyway.locations=db/migration
quarkus.flyway.schemas=public
quarkus.flyway.create-schemas=true

# Placeholders (for dynamic SQL)
quarkus.flyway.placeholders.tablespace=production_ts

# Flyway Table Name
quarkus.flyway.table=flyway_schema_history

Key settings:

  • migrate-at-start: Automatically runs migrations on application startup
  • baseline-on-migrate: Creates baseline for existing databases
  • validate-on-migrate: Ensures migration integrity
  • clean-at-start: ⚠️ DANGER! Drops all database objects (only for testing)

Migration File Naming Convention

Flyway uses a specific naming pattern:

Versioned Migrations:

V{version}__{description}.sql

Examples:
V1.0.0__create_positions_table.sql
V1.0.1__create_products_table.sql
V2.0.0__add_categories.sql

Repeatable Migrations:

R__{description}.sql

Examples:
R__create_product_view.sql
R__create_statistics_function.sql

Execution rules:

  • Versioned migrations run once, in order
  • Repeatable migrations run when their checksum changes
  • Never modify applied migrations (create new ones instead)

Migration V1: Positions Table

Create src/main/resources/db/migration/V1.0.0__create_positions_table.sql:

-- ==========================================
-- POSITIONS TABLE (Tree Structure)
-- ==========================================

CREATE TABLE positions (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    code VARCHAR(50) UNIQUE NOT NULL,
    description TEXT,
    level INTEGER NOT NULL DEFAULT 0,
    parent_id BIGINT,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP,
    
    -- Self-referencing foreign key for tree structure
    CONSTRAINT fk_position_parent 
        FOREIGN KEY (parent_id) 
        REFERENCES positions(id) 
        ON DELETE CASCADE,
    
    -- Business rules
    CONSTRAINT chk_level_positive 
        CHECK (level >= 0),
    
    CONSTRAINT chk_code_format 
        CHECK (code ~ '^[A-Z0-9\\-]+$')
);

-- Performance indexes
CREATE INDEX idx_position_parent ON positions(parent_id);
CREATE INDEX idx_position_code ON positions(code);
CREATE INDEX idx_position_level ON positions(level);
CREATE INDEX idx_position_name ON positions(name);

-- Documentation
COMMENT ON TABLE positions IS 'Hierarchical warehouse infrastructure positions';
COMMENT ON COLUMN positions.level IS '0=Factory, 1=Warehouse, 2=Zone, 3=Aisle, 4=Shelf, 5=Bin';

Design notes:

  • Self-referencing FK: Enables tree structure (parent-child relationships)
  • Indexes: Critical for performance on parent lookups and tree traversal
  • Constraints: Enforce data integrity at database level
  • CASCADE DELETE: Automatically removes child positions when parent is deleted

Migration V2: Products Table

Create src/main/resources/db/migration/V1.0.1__create_products_table.sql:

-- ==========================================
-- PRODUCTS TABLE
-- ==========================================

CREATE TABLE products (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    quantity INTEGER NOT NULL DEFAULT 0,
    unit_of_measurement VARCHAR(50),
    stock INTEGER NOT NULL DEFAULT 0,
    codebar VARCHAR(100) UNIQUE,
    position_id BIGINT,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP,
    
    -- Foreign key to positions
    CONSTRAINT fk_product_position 
        FOREIGN KEY (position_id) 
        REFERENCES positions(id) 
        ON DELETE SET NULL,
    
    -- Business constraints
    CONSTRAINT chk_quantity_positive 
        CHECK (quantity >= 0),
    
    CONSTRAINT chk_stock_positive 
        CHECK (stock >= 0),
    
    CONSTRAINT chk_stock_not_exceed_quantity
        CHECK (stock <= quantity)
);

-- Indexes for common queries
CREATE INDEX idx_product_codebar ON products(codebar);
CREATE INDEX idx_product_position ON products(position_id);
CREATE INDEX idx_product_name ON products(name);
CREATE INDEX idx_product_stock ON products(stock);

-- Full-text search index (PostgreSQL specific)
CREATE INDEX idx_product_name_fts 
    ON products 
    USING gin(to_tsvector('english', name));

-- Documentation
COMMENT ON TABLE products IS 'Product inventory with warehouse positioning';
COMMENT ON COLUMN products.codebar IS 'Barcode/QR code for scanning';

Migration V3: Initial Data

Create src/main/resources/db/migration/V1.0.2__insert_initial_data.sql:

-- ==========================================
-- INITIAL WAREHOUSE STRUCTURE
-- ==========================================

-- Level 0: Factory
INSERT INTO positions (id, name, code, level, parent_id) 
VALUES (1, 'BMW Factory Munich', 'FAC-MUC-001', 0, NULL);

-- Level 1: Warehouses
INSERT INTO positions (id, name, code, level, parent_id) 
VALUES 
    (2, 'Main Warehouse', 'WH-001', 1, 1),
    (3, 'Secondary Warehouse', 'WH-002', 1, 1);

-- Level 2: Zones
INSERT INTO positions (id, name, code, level, parent_id) 
VALUES 
    (4, 'Storage Zone A', 'ZN-A01', 2, 2),
    (5, 'Storage Zone B', 'ZN-A02', 2, 2),
    (6, 'Returns Zone', 'ZN-R01', 2, 3);

-- Level 3: Aisles
INSERT INTO positions (id, name, code, level, parent_id) 
VALUES 
    (7, 'Aisle 1', 'AI-A01-1', 3, 4),
    (8, 'Aisle 2', 'AI-A01-2', 3, 4);

-- Level 4: Shelves
INSERT INTO positions (id, name, code, level, parent_id) 
VALUES 
    (9, 'Shelf Level 1', 'SH-A01-1-1', 4, 7),
    (10, 'Shelf Level 2', 'SH-A01-1-2', 4, 7);

-- Reset sequence
SELECT setval('positions_id_seq', (SELECT MAX(id) FROM positions) + 1);

-- ==========================================
-- SAMPLE PRODUCTS
-- ==========================================

INSERT INTO products (name, quantity, unit_of_measurement, stock, codebar, position_id)
VALUES 
    ('Bolts M8x20', 10000, 'pieces', 8500, 'BOLT-M8-20-001', 9),
    ('Washers M8', 15000, 'pieces', 12000, 'WASH-M8-001', 9),
    ('Engine Oil 5W30', 500, 'liters', 450, 'OIL-5W30-001', 10),
    ('Brake Pads Front', 200, 'sets', 150, 'BRAKE-F-001', 10);

This creates a realistic warehouse hierarchy:

BMW Factory Munich
├── Main Warehouse
│   ├── Storage Zone A
│   │   ├── Aisle 1
│   │   │   ├── Shelf Level 1 (Bolts, Washers)
│   │   │   └── Shelf Level 2 (Oil, Brakes)
│   │   └── Aisle 2
│   └── Storage Zone B
└── Secondary Warehouse
    └── Returns Zone

6. Domain Model Design

Now let's implement the Java entities using Panache.

Position Entity

Create src/main/java/com/bmw/product/model/Position.java:

package com.bmw.product.model;

import io.quarkus.hibernate.orm.panache.PanacheEntity;
import com.fasterxml.jackson.annotation.JsonIgnore;

import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Min;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "positions")
public class Position extends PanacheEntity {
    
    @NotBlank(message = "Position name is required")
    @Column(nullable = false, length = 255)
    public String name;
    
    @NotBlank(message = "Position code is required")
    @Column(unique = true, nullable = false, length = 50)
    public String code;
    
    @Column(columnDefinition = "TEXT")
    public String description;
    
    @Min(value = 0, message = "Level must be non-negative")
    @Column(nullable = false)
    public Integer level = 0;
    
    // Self-referencing relationship for tree structure
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id")
    @JsonIgnore
    public Position parent;
    
    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
    public List<Position> children = new ArrayList<>();
    
    @Column(name = "created_at", nullable = false)
    public LocalDateTime createdAt = LocalDateTime.now();
    
    @Column(name = "updated_at")
    public LocalDateTime updatedAt;
    
    // Custom query methods using Panache
    public static List<Position> findRootPositions() {
        return list("parent is null ORDER BY code");
    }
    
    public static List<Position> findByLevel(int level) {
        return list("level", level);
    }
    
    public static Position findByCode(String code) {
        return find("code", code).firstResult();
    }
    
    // Business logic methods
    public String getFullPath() {
        List<String> path = new ArrayList<>();
        Position current = this;
        while (current != null) {
            path.add(0, current.name);
            current = current.parent;
        }
        return String.join(" > ", path);
    }
}

Key features:

  • PanacheEntity: Provides built-in id field and common methods
  • Tree structure: Self-referencing parent field enables hierarchical data
  • JsonIgnore: Prevents infinite recursion when serializing to JSON
  • Custom queries: Leverages Panache's simplified query API

Product Entity

Create src/main/java/com/bmw/product/model/Product.java:

package com.bmw.product.model;

import io.quarkus.hibernate.orm.panache.PanacheEntity;

import javax.persistence.*;
import javax.validation.constraints.*;
import java.time.LocalDateTime;
import java.util.List;

@Entity
@Table(name = "products")
public class Product extends PanacheEntity {
    
    @NotBlank(message = "Product name is required")
    @Size(min = 3, max = 255)
    @Column(nullable = false, length = 255)
    public String name;
    
    @Min(value = 0, message = "Quantity must be non-negative")
    @Column(nullable = false)
    public Integer quantity = 0;
    
    @Column(name = "unit_of_measurement", length = 50)
    public String unitOfMeasurement;
    
    @Min(value = 0, message = "Stock must be non-negative")
    @Column(nullable = false)
    public Integer stock = 0;
    
    @Column(unique = true, length = 100)
    public String codebar;
    
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "position_id")
    public Position position;
    
    @Column(name = "created_at", nullable = false)
    public LocalDateTime createdAt = LocalDateTime.now();
    
    @Column(name = "updated_at")
    public LocalDateTime updatedAt;
    
    // Custom query methods using Panache
    public static Product findByCodebar(String codebar) {
        return find("codebar", codebar).firstResult();
    }
    
    public static List<Product> findByPosition(Long positionId) {
        return list("position.id", positionId);
    }
    
    public static List<Product> findLowStock(int threshold) {
        return list("stock < ?1 ORDER BY stock ASC", threshold);
    }
    
    // Business logic methods
    public void reduceStock(int amount) {
        if (amount > stock) {
            throw new IllegalArgumentException(
                "Cannot reduce stock by " + amount + ". Only " + stock + " available."
            );
        }
        this.stock -= amount;
        this.updatedAt = LocalDateTime.now();
    }
    
    public void addStock(int amount) {
        if (amount < 0) {
            throw new IllegalArgumentException("Cannot add negative stock");
        }
        this.stock += amount;
        this.quantity += amount;
        this.updatedAt = LocalDateTime.now();
    }
}

7. RESTful API Implementation

Now let's expose our entities through REST endpoints.

ProductResource

Create src/main/java/com/bmw/product/resource/ProductResource.java:

package com.bmw.product.resource;

import com.bmw.product.model.Product;

import javax.transaction.Transactional;
import javax.validation.Valid;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.time.LocalDateTime;
import java.util.List;

@Path("/api/products")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ProductResource {

    @GET
    public List<Product> listAll() {
        return Product.listAll();
    }

    @GET
    @Path("/{id}")
    public Response getById(@PathParam("id") Long id) {
        Product product = Product.findById(id);
        if (product == null) {
            return Response.status(Response.Status.NOT_FOUND)
                .entity("{\\"error\\": \\"Product not found\\"}")
                .build();
        }
        return Response.ok(product).build();
    }

    @GET
    @Path("/barcode/{codebar}")
    public Response findByCodebar(@PathParam("codebar") String codebar) {
        Product product = Product.findByCodebar(codebar);
        if (product == null) {
            return Response.status(Response.Status.NOT_FOUND)
                .entity("{\\"error\\": \\"Product not found\\"}")
                .build();
        }
        return Response.ok(product).build();
    }

    @POST
    @Transactional
    public Response create(@Valid Product product) {
        if (product.codebar != null && Product.findByCodebar(product.codebar) != null) {
            return Response.status(Response.Status.CONFLICT)
                .entity("{\\"error\\": \\"Product with this barcode already exists\\"}")
                .build();
        }
        
        product.createdAt = LocalDateTime.now();
        product.persist();
        
        return Response.status(Response.Status.CREATED)
            .entity(product)
            .build();
    }

    @PUT
    @Path("/{id}")
    @Transactional
    public Response update(@PathParam("id") Long id, @Valid Product updatedProduct) {
        Product product = Product.findById(id);
        if (product == null) {
            return Response.status(Response.Status.NOT_FOUND).build();
        }
        
        product.name = updatedProduct.name;
        product.quantity = updatedProduct.quantity;
        product.stock = updatedProduct.stock;
        product.unitOfMeasurement = updatedProduct.unitOfMeasurement;
        product.codebar = updatedProduct.codebar;
        product.position = updatedProduct.position;
        product.updatedAt = LocalDateTime.now();
        
        return Response.ok(product).build();
    }

    @DELETE
    @Path("/{id}")
    @Transactional
    public Response delete(@PathParam("id") Long id) {
        boolean deleted = Product.deleteById(id);
        if (!deleted) {
            return Response.status(Response.Status.NOT_FOUND).build();
        }
        return Response.noContent().build();
    }
}

PositionResource

Create src/main/java/com/bmw/product/resource/PositionResource.java:

package com.bmw.product.resource;

import com.bmw.product.model.Position;
import com.bmw.product.model.Product;

import javax.transaction.Transactional;
import javax.validation.Valid;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.time.LocalDateTime;
import java.util.List;

@Path("/api/positions")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class PositionResource {

    @GET
    public List<Position> listAll() {
        return Position.listAll();
    }

    @GET
    @Path("/{id}")
    public Response getById(@PathParam("id") Long id) {
        Position position = Position.findById(id);
        if (position == null) {
            return Response.status(Response.Status.NOT_FOUND).build();
        }
        return Response.ok(position).build();
    }

    @GET
    @Path("/{id}/products")
    public List<Product> getProducts(@PathParam("id") Long id) {
        return Product.findByPosition(id);
    }

    @POST
    @Transactional
    public Response create(@Valid Position position) {
        if (Position.findByCode(position.code) != null) {
            return Response.status(Response.Status.CONFLICT)
                .entity("{\\"error\\": \\"Position with this code already exists\\"}")
                .build();
        }
        
        position.createdAt = LocalDateTime.now();
        position.persist();
        
        return Response.status(Response.Status.CREATED)
            .entity(position)
            .build();
    }

    @DELETE
    @Path("/{id}")
    @Transactional
    public Response delete(@PathParam("id") Long id) {
        boolean deleted = Position.deleteById(id);
        if (!deleted) {
            return Response.status(Response.Status.NOT_FOUND).build();
        }
        return Response.noContent().build();
    }
}

API Endpoints Summary

Method Endpoint Description
GET /api/products List all products
GET /api/products/{id} Get product by ID
GET /api/products/barcode/{code} Find by barcode
POST /api/products Create new product
PUT /api/products/{id} Update product
DELETE /api/products/{id} Delete product
GET /api/positions List all positions
GET /api/positions/{id} Get position by ID
GET /api/positions/{id}/products Get products at position
POST /api/positions Create position
DELETE /api/positions/{id} Delete position

8. Environment Configuration

Managing configurations across multiple environments is crucial for production deployments.

Profile-Based Configuration

Development Profile:

# ==========================================
# DEV PROFILE
# ==========================================
%dev.quarkus.datasource.username=postgres
%dev.quarkus.datasource.password=secret
%dev.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/productdb_dev

# Enable SQL logging in dev
%dev.quarkus.hibernate-orm.log.sql=true
%dev.quarkus.hibernate-orm.log.format-sql=true

# Flyway dev settings
%dev.quarkus.flyway.migrate-at-start=true
%dev.quarkus.flyway.clean-at-start=false

# Dev Services: Auto-start PostgreSQL container
%dev.quarkus.datasource.devservices.enabled=true
%dev.quarkus.datasource.devservices.image-name=postgres:15-alpine

Production Profile:

# ==========================================
# PROD PROFILE
# ==========================================

# Use environment variables for credentials
%prod.quarkus.datasource.username=${DB_USERNAME}
%prod.quarkus.datasource.password=${DB_PASSWORD}
%prod.quarkus.datasource.jdbc.url=${DATABASE_URL}

# Production pool settings
%prod.quarkus.datasource.jdbc.max-size=50

# Disable SQL logging
%prod.quarkus.hibernate-orm.log.sql=false

# Flyway production settings
%prod.quarkus.flyway.migrate-at-start=true
%prod.quarkus.flyway.clean-at-start=false

Docker Compose for Local Development

Create docker-compose.yml:

version: '3.8'

services:
  postgres:
    image: postgres:15-alpine
    container_name: product-db
    environment:
      POSTGRES_DB: productdb
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: secret
    ports:
      - "5432:5432"
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

  pgadmin:
    image: dpage/pgadmin4:latest
    container_name: pgadmin
    environment:
      PGADMIN_DEFAULT_EMAIL: admin@example.com
      PGADMIN_DEFAULT_PASSWORD: admin
    ports:
      - "5050:80"
    depends_on:
      - postgres

volumes:
  postgres-data:

Start the environment:

# Start PostgreSQL
docker-compose up -d postgres

# Start PostgreSQL + pgAdmin
docker-compose up -d

# Stop everything
docker-compose down

9. Testing and Monitoring

Database Health Check

Create src/main/java/com/bmw/product/health/DatabaseHealthCheck.java:

package com.bmw.product.health;

import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.Readiness;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.sql.DataSource;
import java.sql.Connection;

@Readiness
@ApplicationScoped
public class DatabaseHealthCheck implements HealthCheck {

    @Inject
    DataSource datasource;

    @Override
    public HealthCheckResponse call() {
        try (Connection connection = datasource.getConnection()) {
            boolean isValid = connection.isValid(5);
            return HealthCheckResponse
                .named("Database connection")
                .status(isValid)
                .withData("database", "PostgreSQL")
                .build();
        } catch (Exception e) {
            return HealthCheckResponse
                .named("Database connection")
                .down()
                .withData("error", e.getMessage())
                .build();
        }
    }
}

Access health endpoints:

# Overall health
curl http://localhost:8080/q/health

# Readiness (includes database check)
curl http://localhost:8080/q/health/ready

# Liveness
curl http://localhost:8080/q/health/live

Integration Tests

Create src/test/java/com/bmw/product/ProductResourceTest.java:

package com.bmw.product;

import io.quarkus.test.junit.QuarkusTest;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.*;

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.*;

@QuarkusTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class ProductResourceTest {

    @Test
    public void testListAllProducts() {
        given()
            .when().get("/api/products")
            .then()
            .statusCode(200)
            .contentType(ContentType.JSON);
    }

    @Test
    public void testCreateProduct() {
        String requestBody = """
            {
                "name": "Test Product",
                "quantity": 100,
                "unitOfMeasurement": "pieces",
                "stock": 80,
                "codebar": "TEST-PROD-001"
            }
            """;

        given()
            .contentType(ContentType.JSON)
            .body(requestBody)
            .when().post("/api/products")
            .then()
            .statusCode(201)
            .body("name", equalTo("Test Product"));
    }
}

Run tests:

./mvnw test

10. Production Deployment

Building the Application

JAR Build:

./mvnw clean package
java -jar target/quarkus-app/quarkus-run.jar

Native Build:

./mvnw package -Pnative
./target/product-management-system-1.0.0-SNAPSHOT-runner

Benefits of native builds:

  • ⚡ Startup time: < 0.1 seconds
  • 💾 Memory usage: ~30MB RSS
  • 📦 No JVM required
  • 🚀 Instant scale-to-zero

Docker Containerization

Build Docker image:

# Build application
./mvnw package

# Build Docker image
docker build -f src/main/docker/Dockerfile.jvm -t product-management-system:latest .

# Run container
docker run -i --rm -p 8080:8080 \\
  -e QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://host.docker.internal:5432/productdb \\
  -e QUARKUS_DATASOURCE_USERNAME=postgres \\
  -e QUARKUS_DATASOURCE_PASSWORD=secret \\
  product-management-system:latest

Kubernetes Deployment

deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: product-management-system
spec:
  replicas: 3
  selector:
    matchLabels:
      app: product-management
  template:
    metadata:
      labels:
        app: product-management
    spec:
      containers:
      - name: product-app
        image: product-management-system:latest
        ports:
        - containerPort: 8080
        env:
        - name: QUARKUS_DATASOURCE_JDBC_URL
          value: jdbc:postgresql://postgres-service:5432/productdb
        - name: QUARKUS_DATASOURCE_USERNAME
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: username
        - name: QUARKUS_DATASOURCE_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: password
        livenessProbe:
          httpGet:
            path: /q/health/live
            port: 8080
        readinessProbe:
          httpGet:
            path: /q/health/ready
            port: 8080

Deploy:

kubectl apply -f k8s/deployment.yaml
kubectl get pods
kubectl logs -f deployment/product-management-system

11. Best Practices and Optimization

Database Best Practices

1. Index Strategy

-- Composite index for common queries
CREATE INDEX idx_product_position_stock 
ON products(position_id, stock) 
WHERE stock < 100;

-- Partial index for active products
CREATE INDEX idx_product_active 
ON products(name, codebar) 
WHERE quantity > 0;

2. Query Optimization

// BAD: N+1 query problem
List<Product> products = Product.listAll();
products.forEach(p -> System.out.println(p.position.name));

// GOOD: Fetch join
@Entity
public class Product extends PanacheEntity {
    public static List<Product> findAllWithPositions() {
        return find("SELECT p FROM Product p LEFT JOIN FETCH p.position").list();
    }
}

3. Connection Pool Tuning

# Formula: (core_count * 2) + effective_spindle_count
quarkus.datasource.jdbc.max-size=20
quarkus.datasource.jdbc.background-validation-interval=2M
quarkus.datasource.jdbc.idle-removal-interval=5M

Flyway Best Practices

1. Never Modify Applied Migrations

# WRONG: Editing V1.0.0 after it's been applied
# RIGHT: Create V1.0.1 with changes

2. Use Semantic Versioning

V1.0.0__initial_schema.sql          # Major
V1.1.0__add_categories.sql          # Minor
V1.1.1__fix_category_constraint.sql # Patch

3. Zero-Downtime Migrations

-- Step 1: Add nullable column
ALTER TABLE products ADD COLUMN category VARCHAR(50);

-- Step 2: Populate defaults
UPDATE products SET category = 'GENERAL' WHERE category IS NULL;

-- Step 3: Make NOT NULL (after deployment)
ALTER TABLE products ALTER COLUMN category SET NOT NULL;

Performance Optimization

1. Enable Second-Level Cache

quarkus.hibernate-orm.second-level-caching-enabled=true
@Entity
@Cacheable
public class Position extends PanacheEntity {
    // Frequently accessed data
}

2. Use Pagination

@GET
public Response listAll(
    @QueryParam("page") @DefaultValue("0") int page,
    @QueryParam("size") @DefaultValue("20") int size
) {
    return Response.ok(
        Product.findAll().page(page, size).list()
    ).build();
}

Security Best Practices

1. Use Environment Variables

# Never hardcode credentials!
quarkus.datasource.password=${DB_PASSWORD}

2. Enable SSL in Production

%prod.quarkus.datasource.jdbc.url=jdbc:postgresql://db:5432/productdb?ssl=true

3. Prevent SQL Injection

// GOOD: Parameterized queries
Product.find("name = ?1", name).list();

// BAD: String concatenation
Product.find("name = '" + name + "'").list(); // NEVER!

12. Conclusion

We've built a comprehensive, production-ready application demonstrating:

Complete datasource configuration with Agroal connection pooling
Hibernate ORM integration with Panache for simplified data access
Flyway-based migrations for version-controlled schema evolution
RESTful CRUD API with proper transaction handling
Tree-based positioning system for warehouse management
Multi-environment configuration for dev, test, and production
Health checks and monitoring for production observability
Docker and Kubernetes deployment strategies

Key Takeaways

  1. Quarkus + PostgreSQL provides a powerful, cloud-native stack
  2. Flyway is essential for managing database schema evolution
  3. Panache dramatically simplifies data access code
  4. Proper configuration management prevents production issues
  5. Native compilation offers incredible performance benefits

Next Steps

To extend this application, consider:

  • 🔐 Authentication & Authorization: Add JWT/OAuth2 security
  • 🔍 Full-Text Search: Implement Elasticsearch
  • 📊 Analytics Dashboard: Build reporting
  • 🔔 Event-Driven Architecture: Add Kafka
  • 📱 Frontend Application: Build React/Angular SPA
  • 🤖 Machine Learning: Implement demand forecasting

Testing the API

Create a product:

curl -X POST http://localhost:8080/api/products \\
  -H "Content-Type: application/json" \\
  -d '{
    "name": "BMW Logo Badge",
    "quantity": 1000,
    "unitOfMeasurement": "pieces",
    "stock": 850,
    "codebar": "BMW-BADGE-001",
    "position": {"id": 9}
  }'

Get all products:

curl http://localhost:8080/api/products

Search by barcode:

curl http://localhost:8080/api/products/barcode/BMW-BADGE-001

Running the Application

# Start PostgreSQL
docker-compose up -d postgres

# Run in dev mode
./mvnw quarkus:dev

# Access:
# - API: http://localhost:8080/api/products
# - Dev UI: http://localhost:8080/q/dev
# - Health: http://localhost:8080/q/health

Additional Resources


Thank you for reading! If you found this guide helpful, please share it with your team. Feel free to leave comments or questions below.

Happy coding with Quarkus! 🚀


Tags: #Quarkus #PostgreSQL #Flyway #Hibernate #REST #Microservices #Java #CloudNative #Docker #Kubernetes


Last updated: July 30, 2026

4/19/2024

QUARKUS - "The new kid on the block"

Here are some useful links related with Quarkus


Quarkus for Spring Developers - Quarkus

Quarkus - OpenAPI Generator :: Quarkiverse Documentation

Writing REST Services with Quarkus REST (formerly RESTEasy Reactive) - Quarkus

Java Web App with Quarkus and JPAStreamer – Tutorial (youtube.com)

Deploying to Kubernetes :: Quarkus Tutorial (redhat-developer-demos.github.io)

Simplified Hibernate ORM with Panache - Quarkus

Home of Quarkus Cheat-Sheet (lordofthejars.github.io)

API-first development with Quarkus (robertopiva.pro)

Quarkus - All configuration options

Microservices with Quarkus – GraphQL API+ Reactive MySQL – Dumi's Blog (wordpress.com)

How to add Swagger to Quarkus - Geeky Hacker

Building REST APIs with Quarkus - Geeky Hacker

My thoughts on Active record pattern - Geeky Hacker

Quarkus for Architects who Sometimes Write Code - Being Persistent - Part 01 - Upstream - Without A Paddle (upstreamwithoutapaddle.com)

Quarkus for Architects who Sometimes Write Code - Being Persistent - Part 03 - Upstream - Without A Paddle (upstreamwithoutapaddle.com)

Getting started with Quarkus |Quarkus Tutorial | Jhooq

Tutorial: Quarkus do Zero até o Deploy no Heroku, utilizando Quarkus Java + REST + CDI + Panache, Hibernate com Postgres + Postman | by Marcus Paulo | Medium

Welcome to Quarkus: Supersonic, Kubernetes-Native Java Framework - Exceptionly

- https://marcelloraffaele.github.io/from-microservices-to-kubernetes-with-quarkus-2/


Validation:

Validation with Hibernate Validator - Quarkus


Errors:

REST API error modeling with Quarkus 2.0 | Red Hat Developer


Properties:

How to bind properties into a Map in Quarkus – Jose Carvajal – Passionate Software Engineer (sgitario.github.io)

-Update Custom Properties in Quarkus at Runtime – Jose Carvajal – Passionate Software Engineer (sgitario.github.io)

How to Debug:

How to debug Quarkus applications - Mastertheboss


Security:

Quarkus Security Securing rest api with HTTPS | Jhooq

GitHub - ilyes12zouaoui/quarkus-microservices-with-jwt

Secure REST APIs in Quarkus using Basic Auth - Geeky Hacker

- Quarkus - Authentication and Authorization With Persistence (blackslate.io)

Authentication and Authorization Using JWT on Quarkus | by Ardiansyah | Medium

Build a Java REST API With Quarkus - DZone


Qute (is like Thymeleaf) :

Quarkus | IntelliJ IDEA Documentation (jetbrains.com)

Quarkus Qute – A Test Ride - Gunnar Morling

Qute Quarkus | Some developer’s blog (rikcarve.github.io)

Quarkus Web Bundler - Main Concepts :: Quarkiverse Documentation

Qute: a Template Engine for Quarkus applications - Mastertheboss

Active record pattern:

My thoughts on Active record pattern - Geeky Hacker


MapStruct :

Using MapStruct within Quarkus. Mapping between beans is always… | by Evren Tan | Developers Keep Learning | Medium

MapStruct and Quarkus - a match made in heaven? – MapStruct


Database & Quarkus:

Connect a Quarkus app to an external SQL Server database | Red Hat Developer

Creating a CRUD shopping service with Quarkus, Hibernate Reactive ORM Panache and PostgreSQL using Active Record Pattern | by David | Geek Culture | Medium

How Quarkus simplifies Java persistence | Red Hat Developer


Testing:

Quarkus Test Framework – Jose Carvajal – Passionate Software Engineer (sgitario.github.io)

Development and Testing of Quarkus applications using Testcontainers

Testing Quarkus Applications | Baeldung

Quarkus Testing with Test Containers – Jose Carvajal – Passionate Software Engineer (sgitario.github.io)

Quarkus tests with Testcontainers and PostgreSQL - Java Code Geeks

Quarkus and Testcontainers - Gunnar MorlingTesting Quarkus Web Applications: Writing Clean Component Tests - InfoQ

Integration Testing using Quarkus, JUnit 5, and Testcontainers. | by Andres Solorzano | Medium


Testcontainers:  

Advanced Testing with Quarkus - Piotr's TechBlog (piotrminkowski.com)


Panache:

Introduction to Panache (thorben-janssen.com)

Quarkus Panache Active Record vs. Repository Pattern - Sebastian Daschner (sebastian-daschner.com)

Data Persistence with Quarkus and Hibernate Panache - Mastertheboss

Quarkus Persistence, CRUD with Panache. | Javarevisited (medium.com)

Part 2: Quarkus Persistence, CRUD with Panache. E-commerce example. | by George Sotiropoulos | Javarevisited | Medium

Split your Monolith into Microservices.How. | Javarevisited (medium.com)

Persistence With Quarkus Panache - Sebastian Daschner (sebastian-daschner.com)

Simplified Hibernate Reactive with Panache - Quarkus

Panache - Active Record Pattern (thorben-janssen.com)

Creating a CRUD shopping service with Quarkus, Hibernate Reactive ORM Panache and PostgreSQL using Active Record Pattern | by David | Geek Culture | Medium

-PanacheEntity (Quarkus - Hibernate ORM with Panache - Runtime 0.19.0 API) (javadoc.io)

Hibernate ORM with Panache in Quarkus - In Relation To


Keycloack:

Getting started with Keycloak powered by Quarkus - Mastertheboss

We look into Keycloak and OpenID using Quarkus (youtube.com)

- https://medium.com/@swechhajha12/setting-up-keycloak-identity-in-your-local-sandbox-a-step-by-step-guide-19a184d69be1

Authentication and authorization using the Keycloak REST API | Red Hat Developer


Other links related:

quarkus-rest 0.1.0 · laminba2003/quarkus-rest (artifacthub.io)

quarkus 0.0.5 · thegusmao/agusmao-charts (artifacthub.io)

API-first development with Quarkus (robertopiva.pro)

How to bind properties into a Map in Quarkus – Jose Carvajal – Passionate Software Engineer (sgitario.github.io)

Deploy Quarkus Apps into the Cloud – Jose Carvajal – Passionate Software Engineer (sgitario.github.io)

Kickstart your first Quarkus application | Quarkus Cookbook (hantsy.github.io)


Videos:

Quarkus for Spring Developers | Red Hat Developer

- Why You Want to Code With Quarkus in 2023 (youtube.com)

Catarina teaser (new logo) (youtube.com)

Run Quarkus inside docker | Dockerizing a Quarkus Application (youtube.com)

Tracing a Quarkus microservice with Jaeger (youtube.com)

Authentication and Authorization using JWT Token and Roles-Based Access Control | Quarkus Tutorial (youtube.com)

OpenId Connect and JSON Web Token Security in Quarkus by Stuart Douglas & Sergey Beryozkin (youtube.com)

Bing Videos


Upload files:

Uploading Files With Quarkus (Update) (youtube.com)


Docker:

Run Quarkus inside docker | Dockerizing a Quarkus Application | Jhooq

Docker Desktop: The #1 Containerization Tool for Developers | Docker

4 Ways to copy file from localhost to docker container | Jhooq

Implementing Docker-based PostgreSQL Database in Quarkus: External PostgreSQL Connection (trycatchdebug.net)

Quarkus remote dev in Docker containers (Update) - Sebastian Daschner (sebastian-daschner.com)

Tutorial: Quarkus App with Docker (htl-leonding-college.github.io) !!!

Kickstart your first Quarkus application | Quarkus Cookbook (hantsy.github.io)

Deploying Quarkus/PostgreSQL and Angular/Nginx on Heroku as containers | by Felipe Windmoller | Medium

Tutorial: Quarkus App with Docker (htl-leonding-college.github.io)

Quarkus remote dev in Docker containers (Update) - Sebastian Daschner (sebastian-daschner.com)

Containerize Java Project. Log : 001 | by Wajeeh Ahmed | Mar, 2024 | Medium

How to Deploy Quarkus App in Docker (youtube.com)

- https://dev.to/marcuspaulo/tutorial-publish-a-quarkus-application-in-kubernetes-minikube-and-dockerhub-36nd


Jaeger :

Demo for using opentracing/jaeger with quarkus | opentracing-quarkus (guhilling.github.io)

MicroProfile-OpenTracing with Supersonic Subatomic Quarkus | by Pavol Loffay | JaegerTracing | Medium

Quarkus - Using OpenTracing

Distributed Tracing with Quarkus, Python, Open Telemetry and Jaeger (Part 1) | by Heiko W. Rupp | ITNEXT

Step by step guide for microservice using Quarkus . | Medium


Grafana:

quarkus-grafana-dashboard/screenshot.png at master · lwitkowski/quarkus-grafana-dashboard · GitHub

-Monitoring Quarkus with Prometheus and Grafana - Exceptionly


Deploy on Cloud:

Deploy Quarkus Apps into the Cloud – Jose Carvajal – Passionate Software Engineer (sgitario.github.io)

Tutorial: Publish a Quarkus application in Kubernetes, Minikube and Dockerhub - DEV Community

Running a Quarkus Native Application on Render - Gunnar Morling

Guide to Quarkus on Kubernetes - Piotr's TechBlog (piotrminkowski.com)

Building and Deploying Cloud-Native Quarkus-based Java Applications to Kubernetes | by Gary A. Stafford | ITNEXT


Monitoring:

Monitoring Quarkus apps with Prometheus into OpenShift – Jose Carvajal – Passionate Software Engineer (sgitario.github.io)

Monitoring Quarkus apps with Prometheus into OpenShift – Jose Carvajal – Passionate Software Engineer (sgitario.github.io)


Microservices:

Implementing Microservicilities with Quarkus and MicroProfile - InfoQ


Deploy :


 ArC 

https://github.com/quarkusio/quarkus/tree/master/independent-projects/arc


CDI limitations 

https://quarkus.io/guides/cdi-reference#limitations


CDI 

https://jcp.org/en/jsr/detail?id=365


JavaBeans 

https://en.wikipedia.org/wiki/JavaBeans 


Expression Language 

https://jakarta.ee/specifications/expression-language 


Observer Pattern 

https://en.wikipedia.org/wiki/Observer_pattern 


Quarkus Configuration 

https://quarkus.io/guides/all-config 


Configuration

https://microprofile.io/project/eclipse/microprofile-config 


Configuration GitHub 

https://github.com/eclipse/microprofile-config 


JUL 

https://docs.oracle.com/en/java/javase/11/docs/api/java.logging/java/util/logging/package-summary.html 


JBoss 

Logging https://github.com/jboss-logging/jboss-logging


SLF4J

 http://www.slf4j.org/ 102


Commons Logging 

https://commons.apache.org/proper/commons-logging 


GELF

 https://www.graylog.org/features/gelf 


Sentry 

https://sentry.io 


Syslog 

https://en.wikipedia.org/wiki/Syslog


Logging format string 

https://quarkus.io/guides/logging#format-string 


SimpleDateFormat https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/text/SimpleDateFormat.htm

Database Persistence and Flyway in Quarkus

Database Persistence and Flyway in Quarkus  Building a Production-Ready Product Management System with PostgreSQL Introduction In ...