Minimal, production-grade sample:
- Angular 16 SPA: CRUD, optimistic updates, CDK virtual scroll, reactive forms, standalone components, strict TS, Jest unit tests, Playwright E2E.
- Spring Boot 3 API: layered architecture, pagination/search, cache hints (ETag/Cache-Control), Bean Validation, RFC7807 Problem Details, Flyway, Testcontainers.
Assumptions (edit as needed): No authentication in the sample; audit user is provided via
X-Actorheader. Default timezone is UTC everywhere.
- Repo layout
- Prerequisites
- Quickstart
- Backend (Spring Boot 3)
- Frontend (Angular 16)
- CI (GitHub Actions)
- Troubleshooting
- Notes & trade-offs
- License
feature-flag-admin/
README.md
docker-compose.yml
backend/
pom.xml
src/main/resources/
application.yml
application-dev.yml
db/migration/
V1__init.sql
frontend/
package.json
angular.json
src/...
.github/workflows/ci.yml
- Java 21+, Node 20+, Docker (Compose v2).
- DB is PostgreSQL 16; system timezone UTC.
docker compose up --build
# SPA http://localhost:4200 (nginx)
# API http://localhost:8080
# DB localhost:5432 (user/pass: ffa/ffa, db: ffa)Terminal A (DB)
docker compose up dbTerminal B (API)
cd backend
./mvnw spring-boot:run -Dspring-boot.run.profiles=devTerminal C (SPA)
cd frontend
pnpm install
pnpm start # http://localhost:4300 (dev server, proxy to backend)cd backend
./mvnw -B -ntp verify
./mvnw spring-boot:run -Dspring-boot.run.profiles=devbackend/src/main/resources/application.yml (trimmed):
spring:
application.name: feature-flag-admin
datasource:
url: jdbc:postgresql://localhost:5432/ffa
username: ffa
password: ffa
jpa:
hibernate.ddl-auto: validate
properties:
hibernate.jdbc.time_zone: UTC
hibernate.format_sql: true
open-in-view: false
flyway.enabled: true
server:
port: 8080
management.endpoints.web.exposure.include: health,info,metricsDev overrides:
# backend/src/main/resources/application-dev.yml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/ffa
jpa:
show-sql: true
logging.level.org.hibernate.SQL: debugV1__init.sql creates f_flag and audit_log tables (PK/UK on key, @Version column, audit JSON, indexes).
- Swagger UI:
http://localhost:8080/swagger-ui/index.html(when Swagger deps included) - Base path:
/api/v1
Entities (conceptual)
{
"Flag": {
"id": "UUID",
"key": "string", // unique, kebab/snake-case
"name": "string",
"description": "string?",
"enabled": true,
"version": 3, // JPA @Version, also exposed via ETag
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-02T00:00:00Z"
}
}Endpoints
GET /flags?page=0&size=20&sort=key,asc&q=searchTerm— paged list (ILIKE on key/name/desc)GET /flags/{id}— returns200withETag: "v{version}"or404POST /flags— create (requires headerX-Actor)PUT /flags/{id}— update (requiresIf-Match: "v{version}"for concurrency)PATCH /flags/{id}— partial update (optional)DELETE /flags/{id}— delete (optimistic locking respected)
Caching/Concurrency
ShallowEtagHeaderFilteraddsETag; clients sendIf-None-Match/If-Match.- GET responses include
Cache-Control: private, max-age=0, must-revalidate.
Problem Details (errors)
- RFC7807 with fields:
type,title,status,detail,instance, plusviolations[]for Bean Validation.
Happy path smoke (PowerShell)
$api='http://localhost:8080/api/v1/flags'
$h=@{'Content-Type'='application/json';'X-Actor'='demo-user'}
$body=@{key="beta_ui_$(Get-Random)";name="Beta UI";enabled=$true}|ConvertTo-Json
$created = irm -Method Post -Uri $api -Headers $h -Body $body
irm ($api + '/' + $created.id) -Headers $h- Unit + slice tests:
./mvnw -B -ntp test - Integration tests: Testcontainers (PostgreSQL) via
*IT.javawith Failsafe. FlagControllerTestcovers 200/404, ETags, paging.
Key dependencies (excerpt):
- Spring Boot: Web, Validation, Data JPA, Actuator, Cache
- DB/tooling: PostgreSQL, Flyway
- Mapping: MapStruct (optional)
- Test: Spring Boot Starter Test, Testcontainers (JUnit 5)
cd frontend
pnpm install
pnpm start # http://localhost:4300 (proxy -> http://localhost:8080)pnm build # dist/ outputpnpm testNotes: jest.config.cjs, setup-jest.ts included. If adjusting structure, update transformIgnorePatterns and setup path accordingly.
One-shot orchestration from repo root (starts API, waits for health, then runs E2E):
.\e2e.ps1The script uses mvnw to boot the API, polls /actuator/health, then runs pnpm dlx playwright test.
Frontend features
- Standalone components, strict TS, RxJS best practices.
- Reactive forms (validation), HTTP interceptors for ETag/
X-Actor, route guards. - CDK Virtual Scroll for lists, optimistic UI updates with server reconciliation.
- Lightweight state services (or NgRx if scaling up).
Backend job (Temurin 21)
- uses: actions/setup-java@v4
with: { distribution: temurin, java-version: '21' }
- run: ./mvnw -B -ntp verify
working-directory: backendFrontend job (Node + pnpm)
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- uses: pnpm/action-setup@v4
with: { version: 10 }
- run: pnpm install
working-directory: frontend
- run: pnpm test && pnpm build
working-directory: frontendUse the wrapper (
mvnw) to pin Maven in CI; the pnpm action avoids “pnpm: command not found”.
-
Flyway checksum mismatch (dev only)
./mvnw -q -Dflyway.clean-disabled=false flyway:repairor revert the migration. -
PowerShell ****************************
Invalid URI
Ensure$apihas no trailing slash; build URLs via($api + '/' + $id). -
Angular/Jest ESM issues
Keepjest.config.cjs; adjusttransformIgnorePatterns; ensuresetup-jest.tspath matches. -
Docker on Windows
If port 4200 is busy, changedocker-compose.ymlmapping or stop local Angular dev server.
- Concurrency: Optimistic UI + JPA
@Versionprovides end-to-end conflict detection. - Caching:
ShallowEtagHeaderFilter+Cache-Controlfor GET;@Cacheableforget(id)(optional). - Search: Simple
qagainst key/name/desc (ILIKE). Extend to advanced filters as needed. - Lombok: Speeds POJOs; be aware of IDE plugin and potential noise in stack traces.
- Mapping: MapStruct gives fast, explicit mappings; manual mapping keeps deps minimal.
MIT License
Copyright (c) 2025 Feature Flag Admin Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.