I needed feature flags for a side project — not a SaaS dashboard, just three things: toggle a feature, roll it out to 10% of users, and target specific customers.

So I built a thin version that works at scale in production.

You can clone it and run it with Docker Compose, or build it step by step. Either way, you'll understand what's happening under the hood — the same mechanics that LaunchDarkly, Unleash, and Flagsmith use internally.

What we're building

A REST API with three capabilities:

  • Toggle flags on/off globally
  • Gradual rollout — enable for 10% of users, then 50%, then everyone
  • Target specific users — all "pro" plan users, specific countries, etc.

Here's what it looks like:

# Create a flag, set it to 30% rollout, and evaluate
curl -X POST http://localhost:8080/flags -H "Content-Type: application/json" \
  -d '{"name":"dark-mode","description":"Dark mode UI"}'

curl -X PUT http://localhost:8080/flags/dark-mode -H "Content-Type: application/json" \
  -d '{"enabled":true,"rolloutPercentage":30}'

curl "http://localhost:8080/flags/evaluate/dark-mode?userId=user-42"
# → {"enabled":true,"reason":"in rollout bucket 12 < 30%"}

curl "http://localhost:8080/flags/evaluate/dark-mode?userId=user-99"
# → {"enabled":false,"reason":"outside rollout bucket 73 >= 30%"}

Same user always gets the same result. That's deterministic hashing — we'll get to how it works.

POST   /flags                  → create a flag
PUT    /flags/{name}           → toggle / configure
GET    /flags/evaluate/{name}  → check if enabled for a user
POST   /flags/{name}/rules     → add targeting rule
GET    /flags/{name}/rules     → list targeting rules

You'll learn:

  • Deterministic cohort assignment for percentage rollouts
  • A data model for rollouts + targeting rules
  • Schema evolution managed as versioned code

Stack: Spring Boot 3, PostgreSQL 16, Flamingock (for schema evolution), Docker Compose.

Time: ~30 minutes if you're comfortable with Spring Boot, or just clone & run.


Flamingock — open-source change-as-code

Quick background on the tool handling schema evolution in this project. Flamingock is an open-source platform for managing how external systems evolve — databases, queues, configs, schemas — as versioned code applied/verified at startup.

If you find the approach useful, a ⭐️ Star to our Flamingock project on GitHub would help us grow.

Star Flamingock on GitHub

Star Flamingock on GitHub


Setup

Option A: Clone and run

git clone https://github.com/flamingock/flamingock-java-samples
cd flamingock-java-samples/feature-flags
docker compose up --build

App starts at localhost:8080. Done.

Option B: Build from scratch

build.gradle:

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.4.4'
    id 'io.spring.dependency-management' version '1.1.7'
    id 'io.flamingock' version '1.0.0'
}

group = 'io.flamingock'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

repositories {
    mavenCentral()
}

flamingock {
    community()
    springboot()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    runtimeOnly 'org.postgresql:postgresql'
}

settings.gradle:

pluginManagement {
    repositories {
        mavenLocal()
        gradlePluginPortal()
    }
}

rootProject.name = 'feature-flags'

Generate the Gradle wrapper (run once from the project root):

gradle wrapper --gradle-version 8.12

docker-compose.yml:

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_DB: flags
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data

  app:
    build: .
    ports:
      - "8080:8080"
    depends_on:
      - db
    environment:
      SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/flags

volumes:
  pgdata:

Dockerfile:

FROM gradle:8-jdk21 AS build
WORKDIR /app
COPY . .
RUN gradle bootJar --no-daemon

FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /app/build/libs/*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

application.yml:

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/flags
    username: postgres
    password: postgres
  jpa:
    hibernate:
      ddl-auto: none  # Flamingock handles all schema changes

FeatureFlagApplication.java — just a standard Spring Boot entry point:

package io.flamingock.flags;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class FeatureFlagApplication {
    public static void main(String[] args) {
        SpringApplication.run(FeatureFlagApplication.class, args);
    }
}

Now let's build.


Part 1 — Simple flags

The simplest possible thing: a flag with a name and an on/off switch.

Entity

package io.flamingock.flags.model;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;

@Entity
@Table(name = "feature_flags")
public class FeatureFlag {

    @Id
    private String name;
    private String description;
    private boolean enabled;

    @Column(name = "created_at")
    private Instant createdAt;

    @Column(name = "updated_at")
    private Instant updatedAt;

    public FeatureFlag() {}

    public FeatureFlag(String name, String description) {
        this.name = name;
        this.description = description;
        this.enabled = false;
        this.createdAt = Instant.now();
        this.updatedAt = Instant.now();
    }

    public String getName() { return name; }
    public String getDescription() { return description; }
    public boolean isEnabled() { return enabled; }
    public Instant getCreatedAt() { return createdAt; }
    public Instant getUpdatedAt() { return updatedAt; }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
        this.updatedAt = Instant.now();
    }
}

Repository

package io.flamingock.flags.repository;

import io.flamingock.flags.model.FeatureFlag;
import org.springframework.data.jpa.repository.JpaRepository;

public interface FlagRepository extends JpaRepository<FeatureFlag, String> {}

Controller

package io.flamingock.flags.controller;

import io.flamingock.flags.model.FeatureFlag;
import io.flamingock.flags.repository.FlagRepository;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/flags")
public class FlagController {

    private final FlagRepository repository;

    public FlagController(FlagRepository repository) {
        this.repository = repository;
    }

    @PostMapping
    public FeatureFlag create(@RequestBody CreateFlagRequest req) {
        return repository.save(new FeatureFlag(req.name(), req.description()));
    }

    @GetMapping
    public List<FeatureFlag> list() {
        return repository.findAll();
    }

    @PutMapping("/{name}")
    public FeatureFlag toggle(
            @PathVariable String name,
            @RequestBody Map<String, Boolean> body) {
        FeatureFlag flag = repository.findById(name).orElseThrow();
        flag.setEnabled(body.get("enabled"));
        return repository.save(flag);
    }

    record CreateFlagRequest(String name, String description) {}
}

That's the feature flag logic — straightforward. But who creates the feature_flags table?

DB schema evolution

We'll evolve this schema three times as the flag system grows. Each change is versioned, applied automatically at startup, and has a rollback. Flamingock handles this — same pattern whether you're changing SQL, config, or any other external system.

Changes are annotated with @Change and @Apply. The @TargetSystem annotation tells Flamingock which external system this change targets — here it's our SQL database. Flamingock injects a Connection that's already part of a managed transaction, so each change is atomic.

package io.flamingock.flags.changes;

import io.flamingock.api.annotations.Apply;
import io.flamingock.api.annotations.Change;
import io.flamingock.api.annotations.Rollback;
import io.flamingock.api.annotations.TargetSystem;

import java.sql.Connection;
import java.sql.Statement;

@TargetSystem(id = "postgres-flags")
@Change(id = "create-flags-table", author = "dev")
public class _0001__CreateFlagsTable {

    @Apply
    public void apply(Connection connection) throws Exception {
        try (Statement stmt = connection.createStatement()) {
            stmt.execute("""
                    CREATE TABLE IF NOT EXISTS feature_flags (
                        name         VARCHAR(255) PRIMARY KEY,
                        description  TEXT,
                        enabled      BOOLEAN DEFAULT FALSE,
                        created_at   TIMESTAMPTZ DEFAULT NOW(),
                        updated_at   TIMESTAMPTZ DEFAULT NOW()
                    )
                    """);
        }
    }

    @Rollback
    public void rollback(Connection connection) throws Exception {
        try (Statement stmt = connection.createStatement()) {
            stmt.execute("DROP TABLE IF EXISTS feature_flags");
        }
    }
}

The _0001__ prefix is a naming convention — Flamingock applies changes in order, so the numbering keeps them sequential.

updated_at is set to NOW() on insert; our Java entity updates it explicitly on every change (see setEnabled(), setRolloutPercentage()).

Now wire up Flamingock. All the configuration lives in one place — the @EnableFlamingock annotation, the target system, and the audit store that tracks which changes have been applied:

package io.flamingock.flags.config;

import io.flamingock.api.annotations.EnableFlamingock;
import io.flamingock.api.annotations.Stage;
import io.flamingock.internal.core.external.store.CommunityAuditStore;
import io.flamingock.store.sql.SqlAuditStore;
import io.flamingock.targetsystem.sql.SqlTargetSystem;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@EnableFlamingock(
    stages = @Stage(location = "io.flamingock.flags.changes")
)
@Configuration
public class FlamingockConfig {

    @Bean
    public SqlTargetSystem sqlTargetSystem(DataSource dataSource) {
        return new SqlTargetSystem("postgres-flags", dataSource);
    }

    @Bean
    public CommunityAuditStore auditStore(SqlTargetSystem sqlTargetSystem) {
        return SqlAuditStore.from(sqlTargetSystem);
    }
}

Try it

# Create a flag
curl -s -X POST http://localhost:8080/flags \
  -H "Content-Type: application/json" \
  -d '{"name":"dark-mode","description":"Enable dark mode UI"}' | jq

# Toggle on
curl -s -X PUT http://localhost:8080/flags/dark-mode \
  -H "Content-Type: application/json" \
  -d '{"enabled":true}' | jq

Working feature flags. But right now it's all-or-nothing: on for everyone, or off for everyone. What if we want to roll out to 10% of users first?


Part 2 — Percentage rollout (this is the fun part)

We need two things:

  1. A rollout_percentage field on the flag
  2. A way to assign users to cohorts consistently — same user, same result every time

Entity update

Add the field to FeatureFlag:

@Column(name = "rollout_percentage")
private int rolloutPercentage = 100;

public int getRolloutPercentage() { return rolloutPercentage; }
public void setRolloutPercentage(int pct) {
    this.rolloutPercentage = pct;
    this.updatedAt = Instant.now();
}

Controller update

@PutMapping("/{name}")
public FeatureFlag update(
        @PathVariable String name,
        @RequestBody UpdateFlagRequest req) {
    FeatureFlag flag = repository.findById(name).orElseThrow();
    if (req.enabled() != null) flag.setEnabled(req.enabled());
    if (req.rolloutPercentage() != null) flag.setRolloutPercentage(req.rolloutPercentage());
    return repository.save(flag);
}

record UpdateFlagRequest(Boolean enabled, Integer rolloutPercentage) {}

Evaluation service

Here's the trick: hash the flag name + user ID. Same input → same hash → same cohort assignment. This is how LaunchDarkly, Unleash, and Flagsmith all do it.

package io.flamingock.flags.service;

import io.flamingock.flags.model.FeatureFlag;
import io.flamingock.flags.repository.FlagRepository;
import org.springframework.stereotype.Service;

import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

@Service
public class EvaluationService {

    private final FlagRepository flagRepository;

    public EvaluationService(FlagRepository flagRepository) {
        this.flagRepository = flagRepository;
    }

    public EvalResult evaluate(String flagName, String userId) {
        FeatureFlag flag = flagRepository.findById(flagName).orElse(null);
        if (flag == null) {
            return new EvalResult(false, "flag not found");
        }

        if (!flag.isEnabled()) {
            return new EvalResult(false, "flag disabled");
        }

        if (flag.getRolloutPercentage() >= 100) {
            return new EvalResult(true, "rollout 100%");
        }

        int bucket = bucket(flagName, userId);
        boolean inRollout = bucket < flag.getRolloutPercentage();
        return new EvalResult(inRollout, inRollout
                ? "in rollout bucket " + bucket + " < " + flag.getRolloutPercentage() + "%"
                : "outside rollout bucket " + bucket + " >= " + flag.getRolloutPercentage() + "%");
    }

    /**
     * Deterministic cohort assignment using SHA-256.
     * Same user + flag always lands in the same bucket.
     */
    private int bucket(String flagName, String userId) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hash = digest.digest((flagName + ":" + userId).getBytes());
            // First 4 bytes → integer → mod 100 → bucket 0–99
            return Math.floorMod(ByteBuffer.wrap(hash).getInt(), 100);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }

    public record EvalResult(boolean enabled, String reason) {}
}

💡The key insight: Because the hash is deterministic, increasing the rollout from 30% to 50% adds new users without removing anyone who was already in. That's why this approach works for gradual rollouts — you're expanding the cohort, not reshuffling it.

Deterministic Cohort Assignment

DB schema evolution

package io.flamingock.flags.changes;

import io.flamingock.api.annotations.Apply;
import io.flamingock.api.annotations.Change;
import io.flamingock.api.annotations.Rollback;
import io.flamingock.api.annotations.TargetSystem;

import java.sql.Connection;
import java.sql.Statement;

@TargetSystem(id = "postgres-flags")
@Change(id = "add-rollout-percentage", author = "dev")
public class _0002__AddRolloutPercentage {

    @Apply
    public void apply(Connection connection) throws Exception {
        try (Statement stmt = connection.createStatement()) {
            stmt.execute(
                "ALTER TABLE feature_flags ADD COLUMN IF NOT EXISTS rollout_percentage INT DEFAULT 100"
            );
        }
    }

    @Rollback
    public void rollback(Connection connection) throws Exception {
        try (Statement stmt = connection.createStatement()) {
            stmt.execute(
                "ALTER TABLE feature_flags DROP COLUMN IF EXISTS rollout_percentage"
            );
        }
    }
}

Second schema evolution — Flamingock tracks what's been applied and only runs new changes.

Add the endpoint:

// In FlagController

@GetMapping("/evaluate/{name}")
public EvaluationService.EvalResult evaluate(
        @PathVariable String name,
        @RequestParam String userId) {
    return evaluationService.evaluate(name, userId);
}

Try it

# Set dark-mode to 30% rollout
curl -s -X PUT http://localhost:8080/flags/dark-mode \
  -H "Content-Type: application/json" \
  -d '{"enabled":true,"rolloutPercentage":30}' | jq

# Test different users — some will be in, some out
curl -s "http://localhost:8080/flags/evaluate/dark-mode?userId=user-001" | jq
# → {"enabled":true,"reason":"in rollout bucket 8 < 30%"}

curl -s "http://localhost:8080/flags/evaluate/dark-mode?userId=user-002" | jq
# → {"enabled":false,"reason":"outside rollout bucket 58 >= 30%"}

# Same user always gets the same result (deterministic)
curl -s "http://localhost:8080/flags/evaluate/dark-mode?userId=user-001" | jq
# → {"enabled":true,"reason":"in rollout bucket 8 < 30%"} ← always

Now we can do gradual rollouts. But sometimes percentage isn't precise enough — "enable for all Pro plan users" or "enable in Germany only."


Part 3 — Targeting rules

A flag can have rules like plan equals pro or country in DE,FR,ES. If any rule matches, the user gets the feature regardless of rollout percentage.

Entity + Repository

package io.flamingock.flags.model;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
import java.util.UUID;

@Entity
@Table(name = "targeting_rules")
public class TargetingRule {

    @Id @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    @Column(name = "flag_name")
    private String flagName;

    private String attribute;   // "plan", "country", "email"
    private String operator;    // "equals", "contains", "in"
    private String value;       // "pro", "DE,FR,ES", "@company.com"

    @Column(name = "created_at")
    private Instant createdAt;

    public TargetingRule() {}

    public TargetingRule(String flagName, String attribute, String operator, String value) {
        this.flagName = flagName;
        this.attribute = attribute;
        this.operator = operator;
        this.value = value;
        this.createdAt = Instant.now();
    }

    public UUID getId() { return id; }
    public String getFlagName() { return flagName; }
    public String getAttribute() { return attribute; }
    public String getOperator() { return operator; }
    public String getValue() { return value; }
}
package io.flamingock.flags.repository;

import io.flamingock.flags.model.TargetingRule;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;
import java.util.UUID;

public interface TargetingRuleRepository extends JpaRepository<TargetingRule, UUID> {
    List<TargetingRule> findByFlagName(String flagName);
}

Updated evaluation

// Add to EvaluationService

private final TargetingRuleRepository ruleRepository;

public EvalResult evaluate(String flagName, String userId, Map<String, String> attrs) {
    FeatureFlag flag = flagRepository.findById(flagName).orElse(null);
    if (flag == null) {
        return new EvalResult(false, "flag not found");
    }

    if (!flag.isEnabled()) {
        return new EvalResult(false, "flag disabled");
    }

    // Targeting rules take priority
    List<TargetingRule> rules = ruleRepository.findByFlagName(flagName);
    for (TargetingRule rule : rules) {
        if (matches(rule, attrs)) {
            return new EvalResult(true, "targeting rule matched: " + rule.getAttribute() + " " + rule.getOperator() + " " + rule.getValue());
        }
    }

    // Fall back to percentage
    if (flag.getRolloutPercentage() >= 100) {
        return new EvalResult(true, "rollout 100%");
    }

    int bucket = bucket(flagName, userId);
    boolean inRollout = bucket < flag.getRolloutPercentage();
    return new EvalResult(inRollout, inRollout
            ? "in rollout bucket " + bucket + " < " + flag.getRolloutPercentage() + "%"
            : "outside rollout bucket " + bucket + " >= " + flag.getRolloutPercentage() + "%");
}

private boolean matches(TargetingRule rule, Map<String, String> attrs) {
    String attrValue = attrs.get(rule.getAttribute());
    if (attrValue == null) return false;

    return switch (rule.getOperator()) {
        case "equals"      -> attrValue.equals(rule.getValue());
        case "contains"    -> attrValue.contains(rule.getValue());
        case "in"          -> Arrays.stream(rule.getValue().split(","))
                                    .map(String::trim)
                                    .anyMatch(v -> v.equals(attrValue));
        case "starts_with" -> attrValue.startsWith(rule.getValue());
        default -> false;
    };
}

Controller additions

@PostMapping("/{name}/rules")
public TargetingRule addRule(@PathVariable String name, @RequestBody AddRuleRequest req) {
    return ruleRepository.save(new TargetingRule(name, req.attribute(), req.operator(), req.value()));
}

@GetMapping("/{name}/rules")
public List<TargetingRule> listRules(@PathVariable String name) {
    return ruleRepository.findByFlagName(name);
}

@GetMapping("/evaluate/{name}")
public EvaluationService.EvalResult evaluate(
        @PathVariable String name,
        @RequestParam String userId,
        @RequestParam Map<String, String> allParams) {
    Map<String, String> attrs = new HashMap<>(allParams);
    attrs.remove("userId");
    return evaluationService.evaluate(name, userId, attrs);
}

record AddRuleRequest(String attribute, String operator, String value) {}

DB schema evolution

package io.flamingock.flags.changes;

import io.flamingock.api.annotations.Apply;
import io.flamingock.api.annotations.Change;
import io.flamingock.api.annotations.Rollback;
import io.flamingock.api.annotations.TargetSystem;

import java.sql.Connection;
import java.sql.Statement;

@TargetSystem(id = "postgres-flags")
@Change(id = "create-targeting-rules", author = "dev")
public class _0003__CreateTargetingRules {

    @Apply
    public void apply(Connection connection) throws Exception {
        try (Statement stmt = connection.createStatement()) {
            stmt.execute("CREATE EXTENSION IF NOT EXISTS \"pgcrypto\"");
            stmt.execute("""
                    CREATE TABLE IF NOT EXISTS targeting_rules (
                        id         UUID DEFAULT gen_random_uuid() PRIMARY KEY,
                        flag_name  VARCHAR(255) REFERENCES feature_flags(name),
                        attribute  VARCHAR(255) NOT NULL,
                        operator   VARCHAR(50) NOT NULL,
                        value      TEXT NOT NULL,
                        created_at TIMESTAMPTZ DEFAULT NOW()
                    )
                    """);
            stmt.execute(
                "CREATE INDEX IF NOT EXISTS idx_targeting_rules_flag_name ON targeting_rules(flag_name)"
            );
        }
    }

    @Rollback
    public void rollback(Connection connection) throws Exception {
        try (Statement stmt = connection.createStatement()) {
            stmt.execute("DROP TABLE IF EXISTS targeting_rules");
        }
    }
}

The pgcrypto extension provides gen_random_uuid() for native UUID generation in Postgres.

That's three schema changes now, each versioned and applied automatically at startup. The schema has evolved alongside the application code — no manual scripts, no drift between what the app expects and what the database has.

Schema Evolution

Try it

# Enable dark-mode for all Pro users
curl -s -X POST http://localhost:8080/flags/dark-mode/rules \
  -H "Content-Type: application/json" \
  -d '{"attribute":"plan","operator":"equals","value":"pro"}' | jq

# Add rule: enable for European countries
curl -s -X POST http://localhost:8080/flags/dark-mode/rules \
  -H "Content-Type: application/json" \
  -d '{"attribute":"country","operator":"in","value":"DE,FR,ES,IT"}' | jq

# Pro user → always enabled (even if outside the 30% rollout)
curl -s "http://localhost:8080/flags/evaluate/dark-mode?userId=user-999&plan=pro" | jq
# → {"enabled":true,"reason":"targeting rule matched: plan equals pro"}

# User in Germany → matches country rule
curl -s "http://localhost:8080/flags/evaluate/dark-mode?userId=user-888&country=DE" | jq
# → {"enabled":true,"reason":"targeting rule matched: country in DE,FR,ES,IT"}

# Free user outside Europe → falls back to percentage rollout
curl -s "http://localhost:8080/flags/evaluate/dark-mode?userId=user-777&plan=free&country=US" | jq
# → {"enabled":false,"reason":"outside rollout bucket 61 >= 30%"}

Final project structure

feature-flags/
├── build.gradle
├── settings.gradle
├── Dockerfile
├── docker-compose.yml
├── src/main/java/io/flamingock/flags/
│   ├── FeatureFlagApplication.java
│   ├── config/
│   │   └── FlamingockConfig.java
│   ├── controller/
│   │   └── FlagController.java
│   ├── service/
│   │   └── EvaluationService.java
│   ├── model/
│   │   ├── FeatureFlag.java
│   │   └── TargetingRule.java
│   ├── repository/
│   │   ├── FlagRepository.java
│   │   └── TargetingRuleRepository.java
│   └── changes/
│       ├── _0001__CreateFlagsTable.java
│       ├── _0002__AddRolloutPercentage.java
│       └── _0003__CreateTargetingRules.java
└── src/main/resources/
    └── application.yml

What's next

This gives you feature flags with real rollout logic and targeting. A few directions to take it:

Audit trail — track who toggled what and when. Every flag change becomes a versioned event.

Environment-aware flags — flags that behave differently in dev, staging, and production. This is where managing the evolution of your flag configuration across environments gets interesting — and where the change-as-code approach starts to shine beyond just SQL.

Scheduled activationactivate_at / deactivate_at timestamps for time-boxed experiments.

I'll cover audit trails and environment-aware configs in the next post.


Full source

Clone it, run it, break it: https://github.com/flamingock/flamingock-java-samples/tree/master/feature-flags

The schema evolution in this project is powered by Flamingock, an open-source change-as-code platform.

⭐ Help us out!

If the approach resonated, star the project on GitHub , and let me also know in the comments

Star Flamingock on GitHub

https://github.com/flamingock/flamingock-java.