Skip to content
Merged
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"]
168 changes: 168 additions & 0 deletions feature-flags-kill-switch/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Feature Flags with Kill Switch and Scheduled Activation — Flamingock + Spring Boot + PostgreSQL

A working example that extends the feature-flag service with a **kill switch** and **scheduled flag activation**, 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 `activate_at`/`deactivate_at` scheduling columns.

## 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}'
```

### 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}'
```

Once the kill switch is on, the flag evaluates to `false` for every user — targeting rules, rollout percentage, and scheduling are all bypassed. 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}'
```

### Schedule a flag

```bash
curl -s -X PUT localhost:8080/flags/dark-mode \
-H "Content-Type: application/json" \
-d '{"enabled":true,"activateAt":"2025-11-28T00:00:00Z","deactivateAt":"2025-11-29T00:00:00Z"}'
```

The flag will evaluate to `false` before `activateAt` (`"not yet active"`) and after `deactivateAt` (`"schedule expired"`). During the window, normal evaluation applies.

## 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__AddScheduledActivation` | Adds the `activate_at` and `deactivate_at` columns |

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. **Not yet active** — if `activate_at` is set and now is before it, returns `false`.
4. **Schedule expired** — if `deactivate_at` is set and now is past it, returns `false`.
5. **Targeting rules** — if any rule matches the user's attributes, returns `true`.
6. **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`.

## 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 # kill switch column
│ └── _0005__AddScheduledActivation.java # scheduled activation columns
├── model/ # JPA entities
├── repository/ # Spring Data repositories
├── service/
│ └── EvaluationService.java # Flag evaluation logic
└── 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