spring-boot4-skill 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,61 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.3.0] - 2026-02-21
9
+
10
+ ### Added
11
+
12
+ - **Skill:** spring-boot-4.md: new section 12 (Rate limiting — Bucket4j, time-window vs @ConcurrencyLimit) and section 13 (Resources & performance — HikariCP/R2DBC pools, Actuator pool metrics, Caffeine cache, performance tuning summary).
13
+ - **Skill:** SKILL.md Reference Files "Load when" extended for rate limiting, connection pools, resource metrics, caching, performance tuning; Quick decision mermaid node for "Rate limit / resources / performance?".
14
+ - **Skill:** spring-framework-7.md Resilience section: note linking to spring-boot-4.md for time-window rate limiting.
15
+ - **README:** Skill reference table: spring-boot-4.md description now includes rate limiting, connection pools, caching, performance.
16
+
17
+ [1.3.0]: https://github.com/AyrtonAldayr/agent-skill-java-spring-framework/compare/v1.2.0...v1.3.0
18
+
19
+ ## [1.2.0] - 2026-02-21
20
+
21
+ ### Added
22
+
23
+ - **Docs:** CHANGELOG.md (Keep a Changelog); README "What you get" example tree, expanded Contributing, npm version badge.
24
+ - **CLI:** Project `description` in wizard and in generated Application.java, build.gradle.kts, pom.xml; friendly error messages (EEXIST, EACCES); "Minimal (API only)" option in wizard.
25
+ - **Skill:** New reference `spring-messaging.md` (Kafka, @KafkaListener, producer/consumer); SKILL.md Quick decision node for Messaging/Kafka; Health groups (readiness/liveness) in spring-boot-4.md; BOM sync note in build-templates.md.
26
+ - **Quality:** Jest config and unit tests for templates and buildContext; `scripts/smoke-cli.sh` and `npm run smoke`; `buildContext` exported for tests.
27
+
28
+ ### Changed
29
+
30
+ - **CLI:** Generator context includes `description` default; success message already showed absolute path.
31
+
32
+ [1.2.0]: https://github.com/AyrtonAldayr/agent-skill-java-spring-framework/compare/v1.1.0...v1.2.0
33
+
34
+ ## [1.1.0] - 2026-02-21
35
+
36
+ ### Added
37
+
38
+ - **Skill:** New reference `spring-security-7.md` — OAuth2 Resource Server, JWT, method security (`@PreAuthorize`), CORS.
39
+ - **Skill:** New reference `troubleshooting-migration.md` — common errors (javax/jakarta, RestTemplate, JSpecify, native), Boot 3→4 migration checklist.
40
+ - **Skill:** "When NOT to use this skill" section and Quick decision (mermaid) in SKILL.md.
41
+ - **Skill:** Spring Boot 4 reference: Testcontainers subsection, secure Actuator exposure, Reactive stack (R2DBC + WebFlux), redirect to spring-security-7.md for OAuth2/JWT.
42
+ - **Skill:** Spring Modulith reference: "Common pitfalls" subsection.
43
+ - **Skill:** Spring Framework 7: note for reactive apps (WebFlux + R2DBC) with link to spring-boot-4.md.
44
+ - **README:** Install options for skill via `npx skills add` and `claude skills install`; skill reference table updated.
45
+
46
+ ### Changed
47
+
48
+ - **Skill:** Reference table in SKILL.md now includes Spring Security 7 and Troubleshooting & migration; triggers listed in body.
49
+
50
+ ## [1.0.0] - 2026-02-21
51
+
52
+ ### Added
53
+
54
+ - **CLI:** Interactive wizard for project name, package, build tool (Gradle KTS / Maven), Java version (25/21/17), Spring Boot version, database (PostgreSQL, MySQL, MongoDB, H2, None), features (Actuator, Security, Validation, Modulith, Native, WebFlux, Docker Compose), and Java preview features.
55
+ - **CLI:** Non-interactive mode (`--no-interactive`) and Maven option (`--maven`).
56
+ - **Templates:** Gradle Kotlin DSL and Maven POM with Spring Boot 4.0.3 BOM; Application.java with JSpecify `@NullMarked`, application.yaml with virtual threads and OTEL, ApplicationTests, optional compose.yaml and Modulith skeleton.
57
+ - **Skill:** Core SKILL.md with mandatory workflow (Analyze → Implement → Optimize → Document), Core Principles table, and reference files (spring-framework-7, spring-boot-4, spring-modulith, build-templates).
58
+ - **npm:** Package `spring-boot4-skill` publishable with `npx spring-boot4-skill`.
59
+
60
+ [1.1.0]: https://github.com/AyrtonAldayr/agent-skill-java-spring-framework/compare/v1.0.0...v1.1.0
61
+ [1.0.0]: https://github.com/AyrtonAldayr/agent-skill-java-spring-framework/releases/tag/v1.0.0
package/README.md CHANGED
@@ -1,7 +1,9 @@
1
1
  # Spring Boot 4 · Java 25 · Spring Framework 7
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/spring-boot4-skill.svg)](https://www.npmjs.com/package/spring-boot4-skill)
4
+
3
5
  > **2026-standard** project scaffolder + Claude Code AI skill for Java / Spring development.
4
- > By [AyrtonAldayr](https://github.com/AyrtonAldayr) · **v1.1.0**
6
+ > By [AyrtonAldayr](https://github.com/AyrtonAldayr) · **v1.3.0**
5
7
 
6
8
  This repository provides two tools in one:
7
9
 
@@ -44,6 +46,26 @@ After answering a few prompts, you get a complete project with:
44
46
  - **`compose.yaml`** *(optional)* — Docker Compose for PostgreSQL 17, MongoDB 7, OTEL Collector
45
47
  - **Spring Modulith module skeleton** *(optional)*
46
48
 
49
+ **Example output** (Gradle, default options):
50
+
51
+ ```
52
+ my-service/
53
+ ├── build.gradle.kts
54
+ ├── settings.gradle.kts
55
+ ├── src/
56
+ │ ├── main/
57
+ │ │ ├── java/<package-path>/
58
+ │ │ │ └── Application.java
59
+ │ │ └── resources/
60
+ │ │ └── application.yaml
61
+ │ └── test/
62
+ │ └── java/<package-path>/
63
+ │ └── ApplicationTests.java
64
+ └── compose.yaml # if Docker Compose was selected
65
+ ```
66
+
67
+ The generator prints the full path of the created project (e.g. `Project created at /path/to/my-service`).
68
+
47
69
  ### Wizard options
48
70
 
49
71
  ```
@@ -112,8 +134,9 @@ Once installed, Claude Code acts as a **Senior Spring Boot 4 architect**:
112
134
  | File | Contents |
113
135
  |---|---|
114
136
  | `skills/java-spring-framework/references/spring-framework-7.md` | All Spring 7 APIs with code examples |
115
- | `skills/java-spring-framework/references/spring-boot-4.md` | Boot 4: native, virtual threads, testing (Testcontainers), reactive stack |
137
+ | `skills/java-spring-framework/references/spring-boot-4.md` | Boot 4: native, virtual threads, testing (Testcontainers), reactive stack, rate limiting, connection pools, caching, performance |
116
138
  | `skills/java-spring-framework/references/spring-security-7.md` | OAuth2 Resource Server, JWT, method security, CORS |
139
+ | `skills/java-spring-framework/references/spring-messaging.md` | Kafka, event-driven, @KafkaListener, producer/consumer |
117
140
  | `skills/java-spring-framework/references/spring-modulith.md` | Module structure, events, integration testing, common pitfalls |
118
141
  | `skills/java-spring-framework/references/build-templates.md` | Complete Gradle KTS + Maven POM templates |
119
142
  | `skills/java-spring-framework/references/troubleshooting-migration.md` | Common errors (javax/jakarta, RestTemplate), Boot 3→4 checklist |
@@ -151,12 +174,17 @@ npx spring-boot4-skill
151
174
 
152
175
  ## Contributing
153
176
 
154
- PRs welcome. Please keep all generated code aligned with:
155
-
156
- - [Spring Boot 4.x docs](https://docs.spring.io/spring-boot/)
157
- - [Spring Framework 7.x docs](https://docs.spring.io/spring-framework/reference/)
158
- - [Spring Modulith docs](https://docs.spring.io/spring-modulith/reference/)
159
- - [JSpecify](https://jspecify.dev/)
177
+ PRs welcome.
178
+
179
+ 1. **Clone and install:** `git clone <repo> && cd agent-skill-java-spring-framework && npm install`
180
+ 2. **Run CLI locally:** `node bin/create-spring-app.js [project-name]` or `node bin/create-spring-app.js my-test --no-interactive`
181
+ 3. **Tests:** `npm test` (unit tests); `npm run smoke` (CLI smoke test).
182
+ 4. **Skill changes:** Keep the structure under `skills/java-spring-framework/` (SKILL.md plus `references/*.md`). Load criteria in the Reference Files table should stay accurate.
183
+ 5. **Generated code** must align with:
184
+ - [Spring Boot 4.x docs](https://docs.spring.io/spring-boot/)
185
+ - [Spring Framework 7.x docs](https://docs.spring.io/spring-framework/reference/)
186
+ - [Spring Modulith docs](https://docs.spring.io/spring-modulith/reference/)
187
+ - [JSpecify](https://jspecify.dev/)
160
188
 
161
189
  ---
162
190
 
@@ -0,0 +1,9 @@
1
+ /** @type {import('jest').Config} */
2
+ module.exports = {
3
+ testEnvironment: 'node',
4
+ testMatch: ['**/*.test.js'],
5
+ transform: {},
6
+ moduleNameMapper: {
7
+ '^(\\.{1,2}/.*)\\.js$': '$1',
8
+ },
9
+ };
package/lib/generator.js CHANGED
@@ -15,11 +15,9 @@ const TEMPLATES_DIR = join(__dirname, '../templates');
15
15
  export async function generateProject(config) {
16
16
  const spinner = ora('Generating project…').start();
17
17
  const projectDir = resolve(process.cwd(), config.projectName);
18
+ const ctx = buildContext(config);
18
19
 
19
20
  try {
20
- // Derive additional config values
21
- const ctx = buildContext(config);
22
-
23
21
  // Create root project directory
24
22
  mkdirSync(projectDir, { recursive: true });
25
23
 
@@ -45,15 +43,21 @@ export async function generateProject(config) {
45
43
 
46
44
  } catch (err) {
47
45
  spinner.fail('Generation failed');
46
+ const msg = err.code === 'EEXIST'
47
+ ? `Directory already exists: ${projectDir}`
48
+ : err.code === 'EACCES'
49
+ ? `Permission denied writing to: ${projectDir}`
50
+ : err.message || String(err);
51
+ console.error(chalk.red(msg));
48
52
  throw err;
49
53
  }
50
54
  }
51
55
 
52
56
  // ---------------------------------------------------------------------------
53
- // Context builder
57
+ // Context builder (exported for tests)
54
58
  // ---------------------------------------------------------------------------
55
59
 
56
- function buildContext(config) {
60
+ export function buildContext(config) {
57
61
  const appName = toPascalCase(config.projectName);
58
62
  const packagePath = config.packageName.replace(/\./g, '/');
59
63
  const features = config.features || [];
@@ -76,6 +80,7 @@ function buildContext(config) {
76
80
 
77
81
  return {
78
82
  ...config,
83
+ description: config.description ?? 'Spring Boot 4 microservice',
79
84
  appName,
80
85
  packagePath,
81
86
  features,
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Unit tests for the generator context builder (buildContext).
3
+ */
4
+
5
+ import { buildContext } from './generator.js';
6
+
7
+ describe('buildContext', () => {
8
+ it('includes projectName, packageName, appName, packagePath', () => {
9
+ const ctx = buildContext({
10
+ projectName: 'my-service',
11
+ packageName: 'com.acme',
12
+ database: 'none',
13
+ features: [],
14
+ });
15
+ expect(ctx.projectName).toBe('my-service');
16
+ expect(ctx.packageName).toBe('com.acme');
17
+ expect(ctx.appName).toBe('MyService');
18
+ expect(ctx.packagePath).toBe('com/acme');
19
+ });
20
+
21
+ it('sets hasJpa true for postgresql, mysql, h2', () => {
22
+ expect(buildContext({ projectName: 'x', packageName: 'c', database: 'postgresql', features: [] }).hasJpa).toBe(true);
23
+ expect(buildContext({ projectName: 'x', packageName: 'c', database: 'mysql', features: [] }).hasJpa).toBe(true);
24
+ expect(buildContext({ projectName: 'x', packageName: 'c', database: 'h2', features: [] }).hasJpa).toBe(true);
25
+ expect(buildContext({ projectName: 'x', packageName: 'c', database: 'none', features: [] }).hasJpa).toBe(false);
26
+ });
27
+
28
+ it('sets hasActuator, hasSecurity, hasValidation from features', () => {
29
+ const ctx = buildContext({
30
+ projectName: 'x',
31
+ packageName: 'c',
32
+ database: 'none',
33
+ features: ['actuator', 'validation'],
34
+ });
35
+ expect(ctx.hasActuator).toBe(true);
36
+ expect(ctx.hasSecurity).toBe(false);
37
+ expect(ctx.hasValidation).toBe(true);
38
+ });
39
+
40
+ it('sets hasModulith, hasNative, hasDockerCompose from features', () => {
41
+ const ctx = buildContext({
42
+ projectName: 'x',
43
+ packageName: 'c',
44
+ database: 'postgresql',
45
+ features: ['modulith', 'docker-compose'],
46
+ });
47
+ expect(ctx.hasModulith).toBe(true);
48
+ expect(ctx.hasNative).toBe(false);
49
+ expect(ctx.hasDockerCompose).toBe(true);
50
+ });
51
+
52
+ it('provides description default when missing', () => {
53
+ const ctx = buildContext({
54
+ projectName: 'x',
55
+ packageName: 'c',
56
+ database: 'none',
57
+ features: [],
58
+ });
59
+ expect(ctx.description).toBe('Spring Boot 4 microservice');
60
+ });
61
+
62
+ it('uses provided description when set', () => {
63
+ const ctx = buildContext({
64
+ projectName: 'x',
65
+ packageName: 'c',
66
+ description: 'My API',
67
+ database: 'none',
68
+ features: [],
69
+ });
70
+ expect(ctx.description).toBe('My API');
71
+ });
72
+
73
+ it('sets modulithDataDep for JPA when modulith and postgresql', () => {
74
+ const ctx = buildContext({
75
+ projectName: 'x',
76
+ packageName: 'c',
77
+ database: 'postgresql',
78
+ features: ['modulith'],
79
+ });
80
+ expect(ctx.modulithDataDep).toContain('modulith-starter-jpa');
81
+ });
82
+ });
package/lib/prompts.js CHANGED
@@ -7,6 +7,7 @@ import inquirer from 'inquirer';
7
7
  const DEFAULTS = {
8
8
  projectName: 'my-service',
9
9
  packageName: 'com.example',
10
+ description: 'Spring Boot 4 microservice',
10
11
  buildTool: 'gradle',
11
12
  javaVersion: '25',
12
13
  bootVersion: '4.0.3',
@@ -88,6 +89,12 @@ export async function runWizard(projectNameArg, options = {}) {
88
89
  ],
89
90
  default: 'postgresql',
90
91
  },
92
+ {
93
+ type: 'confirm',
94
+ name: 'minimal',
95
+ message: 'Minimal project (API only — no Actuator, Security, Validation)?',
96
+ default: false,
97
+ },
91
98
  {
92
99
  type: 'checkbox',
93
100
  name: 'features',
@@ -101,6 +108,7 @@ export async function runWizard(projectNameArg, options = {}) {
101
108
  { name: 'Spring WebFlux (Reactive)', value: 'webflux', checked: false },
102
109
  { name: 'Docker Compose support', value: 'docker-compose', checked: false },
103
110
  ],
111
+ when: (ans) => !ans.minimal,
104
112
  },
105
113
  {
106
114
  type: 'confirm',
@@ -111,5 +119,8 @@ export async function runWizard(projectNameArg, options = {}) {
111
119
  },
112
120
  ]);
113
121
 
122
+ if (answers.minimal) {
123
+ answers.features = [];
124
+ }
114
125
  return answers;
115
126
  }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Unit tests for the template engine (renderTemplate, interpolate, #if, #each).
3
+ * Uses in-memory template strings; does not read from disk.
4
+ */
5
+
6
+ import { readFileSync, writeFileSync, mkdtempSync, rmSync } from 'fs';
7
+ import { join } from 'path';
8
+ import { tmpdir } from 'os';
9
+ import { renderTemplate } from './templates.js';
10
+
11
+ describe('templates', () => {
12
+ let tempDir;
13
+
14
+ beforeEach(() => {
15
+ tempDir = mkdtempSync(join(tmpdir(), 'skill-tpl-'));
16
+ });
17
+
18
+ afterEach(() => {
19
+ rmSync(tempDir, { recursive: true });
20
+ });
21
+
22
+ function createTemplate(content) {
23
+ const path = join(tempDir, 'tpl.txt');
24
+ writeFileSync(path, content);
25
+ return path;
26
+ }
27
+
28
+ describe('interpolate', () => {
29
+ it('replaces {{key}} with context value', () => {
30
+ const path = createTemplate('Hello {{name}}');
31
+ expect(renderTemplate(path, { name: 'World' })).toBe('Hello World');
32
+ });
33
+
34
+ it('replaces multiple placeholders', () => {
35
+ const path = createTemplate('{{a}} and {{b}}');
36
+ expect(renderTemplate(path, { a: 'A', b: 'B' })).toBe('A and B');
37
+ });
38
+
39
+ it('replaces undefined/null with empty string', () => {
40
+ const path = createTemplate('x{{missing}}y');
41
+ expect(renderTemplate(path, {})).toBe('xy');
42
+ });
43
+ });
44
+
45
+ describe('{{#if}}', () => {
46
+ it('includes block when value is truthy', () => {
47
+ const path = createTemplate('{{#if ok}}yes{{/if}}');
48
+ expect(renderTemplate(path, { ok: true })).toBe('yes');
49
+ });
50
+
51
+ it('omits block when value is falsy', () => {
52
+ const path = createTemplate('{{#if ok}}yes{{/if}}');
53
+ expect(renderTemplate(path, { ok: false })).toBe('');
54
+ });
55
+
56
+ it('supports negation {{#if !key}}', () => {
57
+ const path = createTemplate('{{#if !hide}}show{{/if}}');
58
+ expect(renderTemplate(path, { hide: true })).toBe('');
59
+ expect(renderTemplate(path, { hide: false })).toBe('show');
60
+ });
61
+ });
62
+
63
+ describe('{{#each}}', () => {
64
+ it('iterates array and exposes {{item}}', () => {
65
+ const path = createTemplate('{{#each items}}({{item}}){{/each}}');
66
+ expect(renderTemplate(path, { items: ['a', 'b'] })).toBe('(a)(b)');
67
+ });
68
+
69
+ it('returns empty string when key is not an array', () => {
70
+ const path = createTemplate('{{#each items}}x{{/each}}');
71
+ expect(renderTemplate(path, { items: null })).toBe('');
72
+ });
73
+ });
74
+
75
+ it('combines if and interpolate', () => {
76
+ const path = createTemplate('{{name}}{{#if active}} (active){{/if}}');
77
+ expect(renderTemplate(path, { name: 'Foo', active: true })).toBe('Foo (active)');
78
+ expect(renderTemplate(path, { name: 'Bar', active: false })).toBe('Bar');
79
+ });
80
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spring-boot4-skill",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Interactive CLI scaffold for Java 25 / Spring Boot 4.x projects — with a bundled Claude Code skill for AI-assisted development.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,8 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node bin/create-spring-app.js",
11
- "test": "node --experimental-vm-modules node_modules/.bin/jest"
11
+ "test": "node --experimental-vm-modules node_modules/.bin/jest",
12
+ "smoke": "bash scripts/smoke-cli.sh"
12
13
  },
13
14
  "keywords": [
14
15
  "spring-boot",
@@ -40,5 +41,8 @@
40
41
  "commander": "^12.1.0",
41
42
  "inquirer": "^10.1.5",
42
43
  "ora": "^8.1.0"
44
+ },
45
+ "devDependencies": {
46
+ "jest": "^29.7.0"
43
47
  }
44
48
  }
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env bash
2
+ # Smoke test: run CLI in non-interactive mode and verify key files exist.
3
+ set -e
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
7
+ CLI="$REPO_ROOT/bin/create-spring-app.js"
8
+ PROJECT_NAME="smoke-test-project"
9
+ TMP_DIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'skill-smoke')
10
+
11
+ cd "$TMP_DIR"
12
+ node "$CLI" "$PROJECT_NAME" --no-interactive
13
+
14
+ PROJECT_DIR="$TMP_DIR/$PROJECT_NAME"
15
+ if [ ! -d "$PROJECT_DIR" ]; then
16
+ echo "FAIL: Project directory not created: $PROJECT_DIR"
17
+ exit 1
18
+ fi
19
+
20
+ if [ ! -f "$PROJECT_DIR/build.gradle.kts" ]; then
21
+ echo "FAIL: build.gradle.kts not found"
22
+ exit 1
23
+ fi
24
+
25
+ if [ ! -f "$PROJECT_DIR/src/main/resources/application.yaml" ]; then
26
+ echo "FAIL: application.yaml not found"
27
+ exit 1
28
+ fi
29
+
30
+ # Default package is com.example; app name from "smoke-test-project" -> SmokeTestProject
31
+ JAVA_FILE=$(find "$PROJECT_DIR/src/main/java" -name "*Application.java" 2>/dev/null | head -1)
32
+ if [ -z "$JAVA_FILE" ] || [ ! -f "$JAVA_FILE" ]; then
33
+ echo "FAIL: Application main class not found under src/main/java"
34
+ exit 1
35
+ fi
36
+
37
+ echo "PASS: Smoke test — CLI generated project with build.gradle.kts, application.yaml, and Application class."
38
+ rm -rf "$TMP_DIR"
@@ -40,6 +40,10 @@ flowchart TD
40
40
  I -->|Yes| J[build-templates.md]
41
41
  A --> K{Migration or errors?}
42
42
  K -->|Yes| L[troubleshooting-migration.md]
43
+ A --> M{Messaging / Kafka?}
44
+ M -->|Yes| N[spring-messaging.md]
45
+ A --> O{Rate limit / resources / performance?}
46
+ O -->|Yes| P[spring-boot-4.md]
43
47
  ```
44
48
 
45
49
  ## Mandatory Workflow
@@ -77,8 +81,9 @@ Load these as needed — do not load all at once:
77
81
  | Topic | File | Load when |
78
82
  |---|---|---|
79
83
  | Spring Framework 7 APIs | `references/spring-framework-7.md` | Framework-level features: versioning, resilience, JSpecify, SpEL, streaming |
80
- | Spring Boot 4 features | `references/spring-boot-4.md` | Boot auto-config, Actuator, native images, testing, virtual threads |
84
+ | Spring Boot 4 features | `references/spring-boot-4.md` | Boot auto-config, Actuator, native images, testing, virtual threads, rate limiting, connection pools, resource metrics, caching, performance tuning |
81
85
  | Spring Security 7 | `references/spring-security-7.md` | OAuth2 Resource Server, JWT, method security, CORS, authentication/authorization |
86
+ | Messaging (Kafka) | `references/spring-messaging.md` | Kafka, event-driven, messaging, @KafkaListener, producer/consumer |
82
87
  | Spring Modulith | `references/spring-modulith.md` | Domain-driven module design, event-driven architecture |
83
88
  | Build templates | `references/build-templates.md` | Gradle KTS or Maven POM scaffolding with 2026 BOM versions |
84
89
  | Troubleshooting & migration | `references/troubleshooting-migration.md` | Migration from Boot 3, compile/runtime errors (javax/jakarta, RestTemplate, native, null-safety) |
@@ -276,6 +276,8 @@ dependencies {
276
276
  | Micrometer | 1.15.x |
277
277
  | io.spring.dependency-management | 1.1.7 |
278
278
 
279
+ When updating BOM versions, keep the CLI templates under `templates/` and this reference in sync.
280
+
279
281
  ---
280
282
 
281
283
  ## 4. Spring Initializr CLI Quick Start
@@ -16,6 +16,8 @@
16
16
  9. [Docker Compose Support](#9-docker-compose-support)
17
17
  10. [Spring Security 7 Basics](#10-spring-security-7-basics)
18
18
  11. [Reactive Stack (R2DBC + WebFlux)](#11-reactive-stack-r2dbc--webflux)
19
+ 12. [Rate Limiting](#12-rate-limiting)
20
+ 13. [Resources & Performance](#13-resources--performance)
19
21
 
20
22
  ---
21
23
 
@@ -201,6 +203,31 @@ management:
201
203
 
202
204
  Protect actuator in Security 7: allow `health`/`info` for load balancers, require authentication for `metrics`/`prometheus`/`traces`. See `references/spring-security-7.md`.
203
205
 
206
+ ### Health groups (readiness / liveness)
207
+
208
+ Customize health groups for Kubernetes or load balancers. Example: a custom "readiness" group that includes DB and a custom indicator:
209
+
210
+ ```yaml
211
+ management:
212
+ endpoint:
213
+ health:
214
+ show-details: when-authorized
215
+ health:
216
+ livenessstate:
217
+ enabled: true
218
+ readinessstate:
219
+ enabled: true
220
+ db:
221
+ enabled: true
222
+ group:
223
+ readiness:
224
+ include: readinessState,db
225
+ liveness:
226
+ include: livenessState
227
+ ```
228
+
229
+ Expose only the group endpoints (e.g. `/actuator/health/readiness`, `/actuator/health/liveness`) in Security 7 for k8s probes.
230
+
204
231
  ### Dependencies (Micrometer + OTEL)
205
232
 
206
233
  ```kotlin
@@ -421,3 +448,153 @@ Use `Netty` as the default server (Boot chooses it when `spring-boot-starter-web
421
448
  ### R2DBC repositories and WebFlux controllers
422
449
 
423
450
  Define reactive repositories (`ReactiveCrudRepository`) and inject them into `@RestController` or handler functions. Use `ServerWebExchange`, `Mono`, and `Flux` for reactive types. For **RestClient** in a reactive app, use the reactive variant and streaming; see `references/spring-framework-7.md` (Streaming Support) for alignment with Spring 7 APIs.
451
+
452
+ ---
453
+
454
+ ## 12. Rate Limiting
455
+
456
+ **Concurrency vs time-based:** For **limiting concurrent calls per method**, use Spring 7’s built-in `@ConcurrencyLimit(maxConcurrentCalls = N)` — see `references/spring-framework-7.md` (Resilience Annotations). For **rate limiting by time window** (e.g. 100 requests per minute per IP or per user), use an in-app filter/interceptor with a token-bucket implementation or a gateway.
457
+
458
+ ### Time-based rate limit (in application)
459
+
460
+ Use **Bucket4j** (token bucket) with a key per client (e.g. IP or authenticated user). Apply it in a `Filter` or `HandlerInterceptor` and return `429 Too Many Requests` when the bucket is exhausted.
461
+
462
+ **Dependency:**
463
+
464
+ ```kotlin
465
+ // build.gradle.kts
466
+ implementation("com.bucket4j:bucket4j-core:8.10.1")
467
+ ```
468
+
469
+ **Example: filter keyed by IP**
470
+
471
+ ```java
472
+ import io.github.bucket4j.Bandwidth;
473
+ import io.github.bucket4j.Bucket;
474
+ import io.github.bucket4j.Refill;
475
+ import jakarta.servlet.FilterChain;
476
+ import jakarta.servlet.ServletException;
477
+ import jakarta.servlet.http.HttpServletRequest;
478
+ import jakarta.servlet.http.HttpServletResponse;
479
+ import org.springframework.core.annotation.Order;
480
+ import org.springframework.stereotype.Component;
481
+ import org.springframework.web.filter.OncePerRequestFilter;
482
+
483
+ import java.io.IOException;
484
+ import java.time.Duration;
485
+ import java.util.Map;
486
+ import java.util.concurrent.ConcurrentHashMap;
487
+
488
+ @Component
489
+ @Order(1)
490
+ public class RateLimitFilter extends OncePerRequestFilter {
491
+
492
+ private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();
493
+ private static final int CAPACITY = 100;
494
+ private static final Duration REFILL_DURATION = Duration.ofMinutes(1);
495
+
496
+ @Override
497
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
498
+ FilterChain filterChain) throws ServletException, IOException {
499
+ String key = clientKey(request); // e.g. request.getRemoteAddr() or principal name
500
+ Bucket bucket = buckets.computeIfAbsent(key, k -> newBucket());
501
+ if (bucket.tryConsume(1)) {
502
+ filterChain.doFilter(request, response);
503
+ } else {
504
+ response.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS);
505
+ response.getWriter().write("Rate limit exceeded");
506
+ }
507
+ }
508
+
509
+ private static String clientKey(HttpServletRequest request) {
510
+ return request.getRemoteAddr();
511
+ }
512
+
513
+ private static Bucket newBucket() {
514
+ Bandwidth limit = Bandwidth.classic(CAPACITY, Refill.greedy(CAPACITY, REFILL_DURATION));
515
+ return Bucket.builder().addLimit(limit).build();
516
+ }
517
+ }
518
+ ```
519
+
520
+ For **per-user** limits, use a key derived from `SecurityContextHolder.getContext().getAuthentication()` (e.g. principal name) and exempt unauthenticated or health endpoints from the filter if needed.
521
+
522
+ **When to use which:** Use `@ConcurrencyLimit` to cap concurrent executions of a specific method. Use a filter with Bucket4j (or similar) for API-level limits per time window (per IP or per user). If traffic is fronted by **Spring Cloud Gateway**, rate limiting can also be implemented at the edge; that is outside the scope of this reference.
523
+
524
+ ---
525
+
526
+ ## 13. Resources & Performance
527
+
528
+ ### Connection pools (HikariCP, R2DBC)
529
+
530
+ **HikariCP (blocking JDBC)** — Spring Boot configures it by default. Tune in `application.yaml`:
531
+
532
+ ```yaml
533
+ spring:
534
+ datasource:
535
+ hikari:
536
+ maximum-pool-size: 20
537
+ minimum-idle: 5
538
+ connection-timeout: 30000
539
+ idle-timeout: 600000
540
+ max-lifetime: 1800000
541
+ ```
542
+
543
+ See [Spring Boot Data Access](https://docs.spring.io/spring-boot/docs/current/reference/html/data.html#data.sql.datasource.hikari) for all options.
544
+
545
+ **R2DBC** — For reactive apps, configure the connection pool similarly (e.g. `spring.r2dbc.pool.*`). DB health is included in health groups (section 4); use readiness to avoid sending traffic to instances that cannot get a connection.
546
+
547
+ ### Metrics for resources
548
+
549
+ Actuator exposes **HikariCP** metrics (e.g. `hikaricp.connections.active`, `hikaricp.connections.idle`, `hikaricp.connections.pending`). Use these in Prometheus/Grafana to size pools and alert on exhaustion. R2DBC pool metrics follow a similar pattern when the reactive stack is used.
550
+
551
+ ### Caching
552
+
553
+ Use **Spring Cache** with **Caffeine** for in-process caching. Reduces repeated work and database load.
554
+
555
+ **Dependencies:**
556
+
557
+ ```kotlin
558
+ // build.gradle.kts
559
+ implementation("org.springframework.boot:spring-boot-starter-cache")
560
+ implementation("com.github.ben-manes.caffeine:caffeine")
561
+ ```
562
+
563
+ **Enable and configure:**
564
+
565
+ ```java
566
+ @SpringBootApplication
567
+ @EnableCaching
568
+ public class Application { ... }
569
+ ```
570
+
571
+ ```yaml
572
+ # application.yaml
573
+ spring:
574
+ cache:
575
+ cache-names: products,users
576
+ caffeine:
577
+ spec: maximumSize=1000,expireAfterWrite=300s
578
+ ```
579
+
580
+ **Usage:**
581
+
582
+ ```java
583
+ @Service
584
+ public class ProductService {
585
+
586
+ @Cacheable(cacheNames = "products", key = "#id")
587
+ public Product findById(Long id) {
588
+ return productRepository.findById(id).orElseThrow();
589
+ }
590
+ }
591
+ ```
592
+
593
+ Use short TTLs or size limits to avoid stale or unbounded caches.
594
+
595
+ ### Performance tuning (summary)
596
+
597
+ - **Virtual threads** (section 6): Enable for high concurrency with blocking I/O; no need to size a large thread pool.
598
+ - **Pool sizing:** Set HikariCP (or R2DBC pool) size based on observed metrics (connections in use, pending); avoid over-provisioning.
599
+ - **N+1:** Avoid N+1 queries in JPA (e.g. `@EntityGraph`, fetch joins, or DTO projections) so a single request does not open many statements.
600
+ - **Observability:** Use Actuator/Prometheus and traces (section 4) to find bottlenecks (slow endpoints, DB, or external calls) and tune accordingly. No JVM-level tuning (heap, GC) is covered here.
@@ -92,6 +92,8 @@ public class PaymentService {
92
92
  }
93
93
  ```
94
94
 
95
+ For **time-window rate limiting** (e.g. requests per minute per IP or user), see `references/spring-boot-4.md` (Rate limiting).
96
+
95
97
  ---
96
98
 
97
99
  ## 4. Programmatic Bean Registration
@@ -0,0 +1,132 @@
1
+ # Spring Messaging (Kafka) — Boot 4
2
+
3
+ **Spring Boot**: 4.0.x | **Spring Kafka**: aligned with Boot BOM | **Jakarta EE**: 11
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [Dependencies](#1-dependencies)
10
+ 2. [application.yaml](#2-applicationyaml)
11
+ 3. [Consumer — @KafkaListener](#3-consumer--kafkalistener)
12
+ 4. [Producer](#4-producer)
13
+ 5. [Records for payloads](#5-records-for-payloads)
14
+
15
+ ---
16
+
17
+ ## 1. Dependencies
18
+
19
+ Spring Boot 4 BOM manages Spring Kafka. Add the starter:
20
+
21
+ ```kotlin
22
+ // build.gradle.kts
23
+ dependencies {
24
+ implementation("org.springframework.kafka:spring-kafka")
25
+ // or explicitly:
26
+ // implementation("org.springframework.boot:spring-boot-starter-web") // or webflux
27
+ // implementation("org.springframework.kafka:spring-kafka")
28
+ }
29
+ ```
30
+
31
+ For JSON (de)serialization with Jackson 3 (Jakarta):
32
+
33
+ ```kotlin
34
+ implementation("org.springframework.kafka:spring-kafka")
35
+ // Jackson is provided by Boot; ensure jakarta.* for JSON
36
+ ```
37
+
38
+ ---
39
+
40
+ ## 2. application.yaml
41
+
42
+ ```yaml
43
+ spring:
44
+ kafka:
45
+ bootstrap-servers: localhost:9092
46
+ consumer:
47
+ group-id: my-app
48
+ auto-offset-reset: earliest
49
+ key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
50
+ value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
51
+ properties:
52
+ spring.json.trusted.packages: "*"
53
+ producer:
54
+ key-serializer: org.apache.kafka.common.serialization.StringSerializer
55
+ value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
56
+ ```
57
+
58
+ ---
59
+
60
+ ## 3. Consumer — @KafkaListener
61
+
62
+ Use Records for message payloads where possible. Listen on a topic and process with Jakarta and JSpecify where applicable.
63
+
64
+ ```java
65
+ import org.springframework.kafka.annotation.KafkaListener;
66
+ import org.springframework.kafka.support.KafkaHeaders;
67
+ import org.springframework.messaging.handler.annotation.Header;
68
+ import org.springframework.messaging.handler.annotation.Payload;
69
+ import org.springframework.stereotype.Component;
70
+
71
+ @Component
72
+ public class OrderEventsConsumer {
73
+
74
+ @KafkaListener(topics = "orders", groupId = "my-app")
75
+ public void onOrder(@Payload OrderEvent event,
76
+ @Header(KafkaHeaders.RECEIVED_KEY) String key) {
77
+ // process event (OrderEvent as record)
78
+ }
79
+ }
80
+ ```
81
+
82
+ With batch consumption:
83
+
84
+ ```java
85
+ @KafkaListener(topics = "orders", groupId = "my-app", containerFactory = "batchFactory")
86
+ public void onOrders(@Payload List<OrderEvent> events) {
87
+ events.forEach(this::process);
88
+ }
89
+ ```
90
+
91
+ ---
92
+
93
+ ## 4. Producer
94
+
95
+ Inject `KafkaTemplate` and send records. Use `JsonSerializer` for value when configured in application.yaml.
96
+
97
+ ```java
98
+ import org.springframework.kafka.core.KafkaTemplate;
99
+ import org.springframework.stereotype.Service;
100
+
101
+ @Service
102
+ public class OrderEventsProducer {
103
+
104
+ private final KafkaTemplate<String, OrderEvent> kafkaTemplate;
105
+
106
+ public OrderEventsProducer(KafkaTemplate<String, OrderEvent> kafkaTemplate) {
107
+ this.kafkaTemplate = kafkaTemplate;
108
+ }
109
+
110
+ public void send(OrderEvent event) {
111
+ kafkaTemplate.send("orders", event.orderId(), event);
112
+ }
113
+ }
114
+ ```
115
+
116
+ For a simple `KafkaTemplate<String, Object>` with JSON, ensure your payload type is on the trusted packages list for the consumer’s `JsonDeserializer`.
117
+
118
+ ---
119
+
120
+ ## 5. Records for payloads
121
+
122
+ Prefer Java records for event DTOs (Jackson 3 supports them):
123
+
124
+ ```java
125
+ public record OrderEvent(String orderId, String productId, int quantity, java.time.Instant at) {}
126
+ ```
127
+
128
+ Use `jakarta.*` and JSpecify nullability in shared libraries if you need strict null contracts; for internal events, records are often sufficient.
129
+
130
+ ---
131
+
132
+ **Summary:** Use `spring-kafka` with Boot 4 BOM, configure bootstrap servers and (de)serializers in `application.yaml`, and use `@KafkaListener` for consumers and `KafkaTemplate` for producers. Prefer records for event payloads. For Spring Cloud Stream (bindings), see the Spring Cloud Stream docs; the same Kafka dependencies can be used with Stream if you add the appropriate starters.
@@ -9,6 +9,7 @@ plugins {
9
9
 
10
10
  group = "{{packageName}}"
11
11
  version = "0.0.1-SNAPSHOT"
12
+ description = "{{description}}"
12
13
 
13
14
  java {
14
15
  toolchain {
@@ -5,9 +5,10 @@ import org.springframework.boot.SpringApplication;
5
5
  import org.springframework.boot.autoconfigure.SpringBootApplication;
6
6
 
7
7
  /**
8
- * {{appName}} — Spring Boot {{bootVersion}} / Java {{javaVersion}} / Jakarta EE 11
8
+ * {{appName}} — {{description}}
9
+ * Spring Boot {{bootVersion}} / Java {{javaVersion}} / Jakarta EE 11
9
10
  *
10
- * Generated by create-spring-boot4 ({{year}})
11
+ * Generated by spring-boot4-skill ({{year}})
11
12
  */
12
13
  @NullMarked
13
14
  @SpringBootApplication(proxyBeanMethods = false)