Skip to content

Commit ed3041d

Browse files
authored
feat: feature flag first commit (#14)
1 parent 7b8a418 commit ed3041d

23 files changed

Lines changed: 793 additions & 0 deletions

feature-flags/.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.gradle/
2+
build/
3+
!gradle/wrapper/gradle-wrapper.jar
4+
.idea/
5+
*.iml
6+
*.ipr
7+
*.iws
8+
out/
9+
.settings/
10+
.classpath
11+
.project
12+
bin/

feature-flags/.sdkmanrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
java=21-tem

feature-flags/Dockerfile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
FROM gradle:8-jdk21 AS build
2+
WORKDIR /app
3+
COPY . .
4+
RUN gradle bootJar --no-daemon
5+
6+
FROM eclipse-temurin:21-jre
7+
WORKDIR /app
8+
COPY --from=build /app/build/libs/*.jar app.jar
9+
ENTRYPOINT ["java", "-jar", "app.jar"]

feature-flags/README.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Feature Flags with Flamingock + Spring Boot + PostgreSQL
2+
3+
A working example that builds a feature-flag service from scratch using [Flamingock](https://www.flamingock.io) to manage database schema evolution, Spring Boot for the REST API, and PostgreSQL as the backing store.
4+
5+
## Prerequisites
6+
7+
- Java 21 (use `sdk env` if you have [SDKMAN](https://sdkman.io/) installed)
8+
- Docker & Docker Compose
9+
10+
## Quick Start
11+
12+
```bash
13+
docker compose up --build
14+
```
15+
16+
That's it. Postgres starts first (health-checked), then the app boots, Flamingock runs the migrations, and the API is live at `http://localhost:8080`.
17+
18+
### Running locally (without Docker for the app)
19+
20+
```bash
21+
# Start only Postgres
22+
docker compose up db -d
23+
24+
# Run the app
25+
./gradlew bootRun
26+
```
27+
28+
## API
29+
30+
### Create a flag
31+
32+
```bash
33+
curl -s -X POST localhost:8080/flags \
34+
-H "Content-Type: application/json" \
35+
-d '{"name":"dark-mode","description":"Dark mode UI"}'
36+
```
37+
38+
### List all flags
39+
40+
```bash
41+
curl -s localhost:8080/flags
42+
```
43+
44+
### Update a flag (enable + set rollout %)
45+
46+
```bash
47+
curl -s -X PUT localhost:8080/flags/dark-mode \
48+
-H "Content-Type: application/json" \
49+
-d '{"enabled":true,"rolloutPercentage":30}'
50+
```
51+
52+
### Evaluate a flag for a user
53+
54+
```bash
55+
curl -s "localhost:8080/flags/evaluate/dark-mode?userId=user-42"
56+
```
57+
58+
Evaluation is deterministic — the same `userId` always lands in the same rollout bucket (SHA-256 hash).
59+
60+
### Add a targeting rule
61+
62+
```bash
63+
curl -s -X POST localhost:8080/flags/dark-mode/rules \
64+
-H "Content-Type: application/json" \
65+
-d '{"attribute":"plan","operator":"equals","value":"pro"}'
66+
```
67+
68+
### Evaluate with attributes
69+
70+
```bash
71+
curl -s "localhost:8080/flags/evaluate/dark-mode?userId=user-999&plan=pro"
72+
```
73+
74+
When a targeting rule matches, the flag is enabled regardless of rollout percentage.
75+
76+
### List rules for a flag
77+
78+
```bash
79+
curl -s localhost:8080/flags/dark-mode/rules
80+
```
81+
82+
## How Flamingock manages the schema
83+
84+
Instead of `ddl-auto` or hand-written SQL scripts, Flamingock applies versioned, auditable changes at startup:
85+
86+
| Change | What it does |
87+
|--------|-------------|
88+
| `_0001__CreateFlagsTable` | Creates the `feature_flags` table |
89+
| `_0002__AddRolloutPercentage` | Adds the `rollout_percentage` column |
90+
| `_0003__CreateTargetingRules` | Creates the `targeting_rules` table + index |
91+
92+
Each change targets the `postgres-flags` SQL target system and receives a `java.sql.Connection` automatically. Flamingock tracks execution in its audit store so changes run exactly once, even across restarts.
93+
94+
## Targeting rule operators
95+
96+
| Operator | Behaviour |
97+
|----------|-----------|
98+
| `equals` | Exact string match |
99+
| `contains` | Substring match |
100+
| `in` | Comma-separated list membership |
101+
| `starts_with` | Prefix match |
102+
103+
## Project structure
104+
105+
```
106+
feature-flags/
107+
├── docker-compose.yml
108+
├── Dockerfile
109+
├── build.gradle
110+
├── settings.gradle
111+
└── src/main/java/com/example/flags/
112+
├── FeatureFlagApplication.java # @EnableFlamingock entry point
113+
├── config/FlamingockConfig.java # SqlTargetSystem + audit store beans
114+
├── changes/ # Flamingock migrations
115+
├── model/ # JPA entities
116+
├── repository/ # Spring Data repositories
117+
├── service/EvaluationService.java # Flag evaluation logic
118+
└── controller/FlagController.java # REST API
119+
```

feature-flags/build.gradle

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
plugins {
2+
id 'java'
3+
id 'org.springframework.boot' version '3.2.0'
4+
id 'io.spring.dependency-management' version '1.1.4'
5+
id 'io.flamingock' version '1.0.0'
6+
}
7+
8+
flamingock {
9+
community()
10+
springboot()
11+
}
12+
13+
java {
14+
sourceCompatibility = JavaVersion.VERSION_21
15+
targetCompatibility = JavaVersion.VERSION_21
16+
toolchain {
17+
languageVersion = JavaLanguageVersion.of(21)
18+
}
19+
}
20+
21+
repositories {
22+
mavenLocal()
23+
mavenCentral()
24+
}
25+
26+
group = 'com.example'
27+
version = '1.0-SNAPSHOT'
28+
29+
dependencies {
30+
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
31+
implementation 'org.springframework.boot:spring-boot-starter-web'
32+
runtimeOnly 'org.postgresql:postgresql'
33+
}
34+
35+
tasks.withType(JavaCompile).configureEach {
36+
options.compilerArgs.add('-parameters')
37+
}

feature-flags/docker-compose.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
volumes:
2+
pgdata:
3+
4+
services:
5+
db:
6+
image: postgres:16
7+
environment:
8+
POSTGRES_DB: flags
9+
POSTGRES_USER: postgres
10+
POSTGRES_PASSWORD: postgres
11+
ports:
12+
- "5432:5432"
13+
volumes:
14+
- pgdata:/var/lib/postgresql/data
15+
healthcheck:
16+
test: ["CMD-SHELL", "pg_isready -U postgres"]
17+
interval: 5s
18+
timeout: 5s
19+
retries: 5
20+
21+
app:
22+
build: .
23+
ports:
24+
- "8080:8080"
25+
environment:
26+
SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/flags
27+
SPRING_DATASOURCE_USERNAME: postgres
28+
SPRING_DATASOURCE_PASSWORD: postgres
29+
depends_on:
30+
db:
31+
condition: service_healthy
60.6 KB
Binary file not shown.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
distributionBase=GRADLE_USER_HOME
2+
distributionPath=wrapper/dists
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
4+
networkTimeout=10000
5+
zipStoreBase=GRADLE_USER_HOME
6+
zipStorePath=wrapper/dists

feature-flags/gradlew

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

feature-flags/gradlew.bat

Lines changed: 92 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)