Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions feature-flags-kill-switch/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
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"]
175 changes: 175 additions & 0 deletions feature-flags-kill-switch/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# Feature Flags with Kill Switch — Flamingock + Spring Boot + PostgreSQL

A working example that extends the feature-flag service with a **kill switch** and an **audit change log**, using [Flamingock](https://www.flamingock.io) to manage every schema change, Spring Boot for the REST API, and PostgreSQL as the backing store.

This project builds on the [feature-flags](../feature-flags/readme.md) example. The two new Flamingock changes add the `force_disabled` kill-switch column and the `flag_change_log` audit table.

## Prerequisites

- Java 21 (use `sdk env` if you have [SDKMAN](https://sdkman.io/) installed)
- Docker & Docker Compose

## Quick Start

```bash
docker compose up --build
```

Postgres starts first (health-checked), then the app boots, Flamingock runs all five migrations, and the API is live at `http://localhost:8080`.

### Running locally (without Docker for the app)

```bash
# Start only Postgres
docker compose up db -d

# Run the app
./gradlew bootRun
```

## API

### Create a flag

```bash
curl -s -X POST localhost:8080/flags \
-H "Content-Type: application/json" \
-d '{"name":"dark-mode","description":"Dark mode UI"}'
```

### List all flags

```bash
curl -s localhost:8080/flags
```

### Update a flag (enable + set rollout %)

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

### Evaluate a flag for a user

```bash
curl -s "localhost:8080/flags/evaluate/dark-mode?userId=user-42"
```

Evaluation is deterministic — the same `userId` always lands in the same rollout bucket (SHA-256 hash).

### Add a targeting rule

```bash
curl -s -X POST localhost:8080/flags/dark-mode/rules \
-H "Content-Type: application/json" \
-d '{"attribute":"plan","operator":"equals","value":"pro"}'
```

### Evaluate with attributes

```bash
curl -s "localhost:8080/flags/evaluate/dark-mode?userId=user-999&plan=pro"
```

When a targeting rule matches, the flag is enabled regardless of rollout percentage.

### List rules for a flag

```bash
curl -s localhost:8080/flags/dark-mode/rules
```

### Activate the kill switch

```bash
curl -s -X PUT localhost:8080/flags/dark-mode \
-H "Content-Type: application/json" \
-d '{"forceDisabled":true,"changedBy":"ops-team"}'
```

Once the kill switch is on, the flag evaluates to `false` for every user — targeting rules and rollout percentage are bypassed entirely. The reason returned is `"kill switch active"`.

### Deactivate the kill switch

```bash
curl -s -X PUT localhost:8080/flags/dark-mode \
-H "Content-Type: application/json" \
-d '{"forceDisabled":false,"changedBy":"ops-team"}'
```

### View the audit log for a flag

```bash
curl -s localhost:8080/flags/dark-mode/log
```

Every change to `enabled`, `rolloutPercentage`, or `forceDisabled` is recorded with the actor (`changedBy`) and a human-readable detail string.

## How Flamingock manages the schema

Instead of `ddl-auto` or hand-written SQL scripts, Flamingock applies versioned, auditable changes at startup:

| Change | What it does |
|--------|-------------|
| `_0001__CreateFlagsTable` | Creates the `feature_flags` table |
| `_0002__AddRolloutPercentage` | Adds the `rollout_percentage` column |
| `_0003__CreateTargetingRules` | Creates the `targeting_rules` table + index |
| `_0004__AddKillSwitch` | Adds the `force_disabled` column to `feature_flags` |
| `_0005__CreateFlagChangeLog` | Creates the `flag_change_log` audit table + index |

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.

## Evaluation logic

Flags are evaluated in priority order:

1. **Kill switch** — if `force_disabled` is `true`, returns `false` immediately.
2. **Disabled** — if `enabled` is `false`, returns `false`.
3. **Targeting rules** — if any rule matches the user's attributes, returns `true`.
4. **Rollout bucket** — deterministic SHA-256 hash of `flagName:userId` maps the user to a 0–99 bucket; returns `true` if the bucket is below `rolloutPercentage`.

## Audit log actions

| Action | Triggered when |
|--------|---------------|
| `ENABLED` | Flag is turned on |
| `DISABLED` | Flag is turned off |
| `ROLLOUT_UPDATED` | Rollout percentage changes |
| `KILL_SWITCH_ON` | Kill switch activated |
| `KILL_SWITCH_OFF` | Kill switch deactivated |

## Targeting rule operators

| Operator | Behaviour |
|----------|-----------|
| `equals` | Exact string match |
| `contains` | Substring match |
| `in` | Comma-separated list membership |
| `starts_with` | Prefix match |

## Project structure

```
feature-flags-kill-switch/
├── docker-compose.yml
├── Dockerfile
├── build.gradle
├── settings.gradle
└── src/main/java/io/flamingock/flags/
├── FeatureFlagApplication.java # @EnableFlamingock entry point
├── config/FlamingockConfig.java # SqlTargetSystem + audit store beans
├── changes/ # Flamingock migrations
│ ├── _0001__CreateFlagsTable.java
│ ├── _0002__AddRolloutPercentage.java
│ ├── _0003__CreateTargetingRules.java
│ ├── _0004__AddKillSwitch.java # NEW: kill switch column
│ └── _0005__CreateFlagChangeLog.java # NEW: audit log table
├── model/ # JPA entities
├── repository/ # Spring Data repositories
├── service/
│ ├── EvaluationService.java # Flag evaluation logic (kill switch aware)
│ └── FlagChangeLogService.java # Audit log recording
└── controller/FlagController.java # REST API
```
34 changes: 34 additions & 0 deletions feature-flags-kill-switch/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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'
}

tasks.withType(JavaCompile).configureEach {
options.compilerArgs.add('-parameters')
}
31 changes: 31 additions & 0 deletions feature-flags-kill-switch/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
volumes:
pgdata:

services:
db:
image: postgres:16
environment:
POSTGRES_DB: flags
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5

app:
build: .
ports:
- "8080:8080"
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/flags
SPRING_DATASOURCE_USERNAME: postgres
SPRING_DATASOURCE_PASSWORD: postgres
depends_on:
db:
condition: service_healthy
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
2 changes: 2 additions & 0 deletions feature-flags-kill-switch/gradlew

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

92 changes: 92 additions & 0 deletions feature-flags-kill-switch/gradlew.bat

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions feature-flags-kill-switch/settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
pluginManagement {
repositories {
mavenLocal()
gradlePluginPortal()
}
}

rootProject.name = 'feature-flags-kill-switch'
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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);
}
}
Loading