spring-di-configuration
Skill18 starsChanged 4 months agoMITView on GitHub
Spring dependency injection and application configuration for Kotlin backends - constructor injection, stereotype annotations, @Configuration and @Bean factories, typed immutable @ConfigurationProperties, profiles and application.yml layout, secrets via environment variables, the injected Clock, stateless singleton beans, and Spring proxy pitfalls (self-invocation with @Transactional, @Cacheable, @Async). Use this skill whenever creating a bean, injecting a dependency, adding a config value or property, writing a @Configuration class, touching application.yml or a profile, wiring a third-party SDK, or reading the current time. Trigger on phrases like "inject", "bean", "@Component", "@Service", "@Configuration", "@Bean", "@Value", "@ConfigurationProperties", "application.yml", "profile", "property", "environment variable", "secret", "Clock", "Instant.now", "@PostConstruct", "self-invocation", or "proxy".
In this repository67 other files it gives its agents.
- agent-onboardingSkill
- android-backgroundSkill
- android-cliSkill
- android-compose-architectureSkill
- android-compose-componentsSkill
- android-data-layerSkill
- android-di-koinSkill
- android-error-handlingSkill
- android-integration-testingSkill
- android-jacoco-setupSkill
- android-jvm-test-coverageSkill
- android-module-structureSkill
- android-navigationSkill
- android-presentation-mviSkill
- android-preview-figma-verifySkill
- android-ui-screenshot-verifySkill
- android-ui-testingSkill
- android-ui-verifySkill
- android-unit-testingSkill
- android-version-catalogSkill
- architecture-decision-recordSkill
- careful-planningSkill
- compose-adaptive-layoutsSkill
- figma-to-android-vectorSkill
- finalizeSkill
- git-commitSkill
- handoffSkill
- kmp-compose-architectureSkill
- kmp-compose-componentsSkill
- kmp-data-layerSkill
- kmp-di-koinSkill
- kmp-error-handlingSkill
- kmp-integration-testingSkill
- kmp-migration-analyzerSkill
- kmp-module-structureSkill
- kmp-navigationSkill
- kmp-presentation-mviSkill
- kmp-ui-testingSkill
- kmp-unit-testingSkill
- kotlin-coroutinesSkill
- kotlin-flowsSkill
- kotlin-mutation-testingSkill
- openspec-artifact-review-lightSkill
- openspec-artifact-reviewSkill
- project-breakdownSkill
- react-data-layerSkill
- react-error-handlingSkill
- react-integration-testingSkill
- react-presentation-mviSkill
- react-project-structureSkill
- react-routingSkill
- react-screen-architectureSkill
- react-securitySkill
- react-server-stateSkill
- react-ui-componentsSkill
- react-unit-testingSkill
- speckit-parallelize-agent-teamsSkill
- speckit-parallelize-subagentsSkill
- spring-api-layerSkill
- spring-integration-testingSkill
- The first 60 shown.
The file
We split this skill into the rules it gives an agent
8 rules. About 0 are common, 0 are distinctive, and 8 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
8
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 11
Constructor injection only: Every dependency is a private val constructor parameter.
- Very specific: line 49
When two beans implement one interface, the interface is too broad.
- Very specific: line 57
Use a @Bean method when the bean is a third-party type, needs construction logic, or needs initialization:
- Very specific: line 133
Use Duration, DataSize, enums, and lists instead of Long minutes or comma-separated strings.
- Very specific: line 154
Always set in application.yml:
- Very specific: line 169
Every class that reads the current time takes a java.time.Clock in its constructor:
- Very specific: line 177
Default parameter values (createdAt: Instant = Instant.now()) on entities and DTOs are not allowed either.
- Very specific: line 186
If a bean must hold state (a WebSocket session registry, an in-memory retry queue), use java.util.concurrent types (ConcurrentHashMap, …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Spring DI and Configuration
Principles
- Constructor injection only. Every dependency is a
private valconstructor parameter. Nolateinit varwith@Autowired, no@Valueon properties, no setter injection. - Beans are stateless singletons. Request-specific data lives in local variables and parameters, never in fields.
- Configuration is typed. Config values are bound into immutable
@ConfigurationPropertiesclasses, never read one by one with@Value. - Time comes from an injected
Clock. NoInstant.now()without a clock. - Secrets come from environment variables. They never appear in a committed file.
Stereotypes
| Class | Annotation | Layer |
| Business logic, transactions | @Service | service |
| Repository adapter, HTTP client adapter, SDK wrapper | @Component | infra |
| REST controller | @RestController | api |
| Event listener, WebSocket handler, exception handler | @Component / @RestControllerAdvice | api |
| Factory for beans you can't annotate (third-party types) | @Configuration(proxyBeanMethods = false) + @Bean | wherever the bean belongs |
@Repository is not used. Spring Data interfaces need no annotation, and adapter classes are plain @Components.
Constructor Injection
@Service
class ChatService(
private val chatRepository: ChatRepository,
private val chatParticipantRepository: ChatParticipantRepository,
private val eventPublisher: ApplicationEventPublisher,
private val clock: Clock,
) {
// ...
}- Inject the domain interface (
ChatRepository), never theinfraclass that implements it. Spring finds the single@Componentthat implements the interface. - When two beans implement one interface, the interface is too broad. Split it before reaching for
@Qualifieror@Primary. - More than ~6 constructor parameters means the class does too much. Split it.
- Circular dependencies are a design error. Spring Boot refuses to start with one. Never enable
spring.main.allow-circular-references.
@Configuration and @Bean
Use a @Bean method when the bean is a third-party type, needs construction logic, or needs initialization:
@Configuration(proxyBeanMethods = false)
class FirebaseConfig {
@Bean
fun firebaseMessaging(properties: FirebaseProperties, resourceLoader: ResourceLoader): FirebaseMessaging {
val credentials = resourceLoader.getResource(properties.credentialsPath).inputStream.use {
GoogleCredentials.fromStream(it)
}
val app = FirebaseApp.initializeApp(
FirebaseOptions.builder().setCredentials(credentials).build()
)
return FirebaseMessaging.getInstance(app)
}
}proxyBeanMethods = falsealways. Beans reference each other through method parameters, never by calling another@Beanmethod.- Initialize SDKs in a
@Beanmethod, not in a@PostConstructon a service. The consumer then injects the ready SDK client, and a test can provide a different bean. - A
@Configurationclass lives in the layer of the beans it creates:infra/configfor SDK clients,:appfor cross-cutting beans.
Cross-cutting beans in :app
@Configuration(proxyBeanMethods = false)
@EnableScheduling
@EnableAsync
class ApplicationConfig {
@Bean
fun clock(): Clock = Clock.systemUTC()
}@SpringBootApplication
@ConfigurationPropertiesScan
class ChirpApplication
fun main(args: Array<String>) {
runApplication<ChirpApplication>(*args)
}Typed Configuration Properties
One properties class per concern, immutable, validated at startup:
@ConfigurationProperties(prefix = "chirp.email")
@Validated
data class EmailProperties(
@field:NotBlank
val from: String,
@field:NotBlank
val baseUrl: String,
val verificationExpiry: Duration = Duration.ofHours(24),
val passwordResetExpiry: Duration = Duration.ofMinutes(30),
)chirp:
email:
from: mail@example.com
base-url: "https://chirp.example.com"
verification-expiry: 24h
password-reset-expiry: 30mvalproperties and a single constructor. Spring binds through the constructor; novar, no@Configurationon the class.@ConfigurationPropertiesScanon the application class registers every properties class. Don't add@EnableConfigurationPropertiesper class.- Use
Duration,DataSize, enums, and lists instead ofLongminutes or comma-separated strings. The unit then lives in the YAML value (30m), not in the property name (expiry-minutes). - Prefix:
<app>.<module>.<concern>for own properties (chirp.chat.storage). Third-party SDK settings follow the same scheme (chirp.notification.firebase), never a bare top-level key likefirebase:. - Inject the properties class where the values are needed. Properties classes live in the layer that uses them, usually
infra/configorservice.
@Value is acceptable only in a @Bean method parameter of a @Configuration class for a single one-off value. If you need two values from the same prefix, write a properties class.
application.yml Layout
app/src/main/resources/
├── application.yml ← Shared defaults for every environment
├── application-dev.yml ← Local development overrides
└── application-prod.yml ← Production overridesapplication.ymlholds the structure and safe defaults. Profile files only override.- Secrets are only ever
${ENV_VAR}references with no default, so a missing secret fails at startup instead of running with a blank one:password: ${POSTGRES_PASSWORD}. - Hosts, ports, and URLs that differ per environment go into the profile file or an env var. Values that are the same everywhere go into
application.yml. - Business modules ship no
application.yml. Their config lives in:appunder the module's prefix, so one place shows the whole app's configuration. - Always set in
application.yml:
spring:
threads:
virtual:
enabled: true
jpa:
open-in-view: falseTime — the Injected Clock
Every class that reads the current time takes a java.time.Clock in its constructor:
val expiresAt = Instant.now(clock).plus(properties.verificationExpiry)- Production gets
Clock.systemUTC()fromApplicationConfig. Tests pass a fake clock (see spring-unit-testing). - Domain models that need "now" for a check get it as a parameter:
fun isExpired(now: Instant) = now >= expiresAt. They never read a clock themselves. - Default parameter values (
createdAt: Instant = Instant.now()) on entities and DTOs are not allowed either. The service sets the timestamp from its clock.
Stateless Singletons
A singleton bean is called by many request threads at once.
- No
varfields and nomutableListOf()/HashMapfields in a bean. - If a bean must hold state (a WebSocket session registry, an in-memory retry queue), use
java.util.concurrenttypes (ConcurrentHashMap,ConcurrentHashMap.newKeySet(), atomics) and keep every compound update inside one atomic call (compute,merge). - State that must survive a restart or be shared between instances belongs in the database or Redis, not in a bean.
Proxy Pitfalls
@Transactional, @Cacheable, @CacheEvict, @Async, and @Retryable work through Spring proxies. A call from one method of a bean to another method of the same bean bypasses the proxy, and the annotation silently does nothing.
@Service
class ChatMessageService(...) {
fun deleteMessage(...) {
// ...
evictCache(chatId) // ❌ self-invocation: @CacheEvict is ignored
}
@CacheEvict("messages", key = "#chatId")
fun evictCache(chatId: ChatId) = Unit
}Fix it by moving the annotated method to the bean whose job it is, and calling that bean. Don't inject a bean into itself, and don't use AopContext.currentProxy().
Annotated methods must also be public and in an open class. The kotlin-spring compiler plugin opens @Component, @Service, @Configuration, and @Transactional classes automatically, so don't add open by hand.
Naming Conventions
| Thing | Convention | Example |
@Configuration class | <Concern>Config | FirebaseConfig, SecurityConfig |
@ConfigurationProperties class | <Concern>Properties | EmailProperties, StorageProperties |
| Property prefix | <app>.<module>.<concern> | chirp.chat.storage |
| Environment variable | SCREAMING_SNAKE_CASE | SUPABASE_SERVICE_KEY |
| Implementation of a domain interface | describe what makes it unique, no Impl | JpaChatRepository, SupabaseFileStorage |
Checklist: Adding a Bean or Config Value
- Dependencies are constructor parameters typed as domain interfaces where one exists
- No mutable fields, or only thread-safe ones with atomic updates
- Config values bound through a
<Concern>Propertiesclass with@Validated, never@Valuefields - Secrets referenced as
${ENV_VAR}with no default - The current time comes from an injected
Clock - No call from a bean into its own
@Transactional/@Cacheable/@Asyncmethod
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

