Set of Spring Boot Best Practices converted into Konsist tests. Helps you write clean and maintainable code and prevent AI slop (at least trying) in your codebase.
Each rule is a static analysis check that runs as a regular JUnit 5 test. Rules inspect your source code structure using Konsist — they look at class names, annotations, package locations, and layer dependencies without compiling or running your application. You configure which rules to enforce via a Kotlin DSL, and the test fails with a clear message identifying the exact class or method that violated the rule.
Maven Central: https://central.sonatype.com/artifact/dev.protsenko/spring-boot-code-guard/overview
Add dependency to your build.gradle.kts or build.gradle:
implementation("dev.protsenko:spring-boot-code-guard:1.0.8")Add a Konsist test file to your project (e.g. src/test/kotlin/SpringCodeGuardTest.kt) and call springBootRules { }.verify() inside a JUnit 5 test.
Enable every available rule with a single call:
@Test
fun `spring boot rules`() {
springBootRules {
all()
}.verify()
}Pick only the rules relevant to your project:
@Test
fun `spring boot rules`() {
springBootRules {
general {
noFieldInjection()
// ... and other rules
}
jpa {
entitiesHaveIdField()
// ... and other rules
}
naming {
serviceNamingConvention()
// ... and other rules
}
packages {
packageNamingConvention()
// ... and other rules
}
web {
properHttpMethodAnnotations()
// ... and other rules
}
}.verify()
}When a rule fires, the violation message is prefixed with its rule ID:
CodeGuard:noFieldInjection: Found 1 field(s) with @Autowired/@Inject in: LegacyService. Use constructor injection instead.
To suppress a rule for a specific class, annotate it with @Suppress using that ID:
@Suppress("CodeGuard:noFieldInjection")
@Service
class LegacyService {
@Autowired
lateinit var repo: UserRepository
}Suppression is class-level — it silences all checks from the given rule for that class and its members.
To disable a rule entirely for the whole test, use exclude() inside the DSL block with the rule's suppress key:
@Test
fun `spring boot rules`() {
springBootRules {
all()
exclude("CodeGuard:noFieldInjection")
}.verify()
}Multiple rules can be excluded at once:
springBootRules {
all()
exclude(
"CodeGuard:noFieldInjection",
"CodeGuard:transactionalPlacement",
)
}.verify()exclude() can be placed anywhere in the block — before or after rule registration. Exclusions are applied at verify() time regardless of order.
Passing an unknown key throws an error listing all registered rule keys. Excluding every registered rule also throws — at least one rule must remain active.
CodeGuard:statelessConfiguration:@Configurationclasses must not declare mutable (var) properties (excluding@Valueand@ConfigurationPropertiesfields) — configuration classes should only define beans and remain side-effect free.CodeGuard:beanMethodsInConfiguration:@Beanmethods must be declared inside@Configurationclasses; placing them in other classes bypasses Spring's scoping and CGLIB proxy mechanisms.CodeGuard:customExceptionStructure: Classes whose name ends withExceptionmust extendRuntimeException, a well-known subtype (IllegalArgumentException,IllegalStateException,ResponseStatusException,NestedRuntimeException), or another custom exception class — not the rawExceptionhierarchy.CodeGuard:noFieldInjection: Fields must not be annotated with@Autowiredor@Inject; constructor injection must be used instead to make dependencies explicit and improve testability.CodeGuard:loggerInsteadOfPrint: Spring bean classes must not callprintlnorprint; use SLF4J or another structured logging framework so output is observable and controllable in production.CodeGuard:noStackTracePrint: Spring bean classes must not callprintStackTrace()directly; use structured logging so stack traces are captured by the application logging pipeline.CodeGuard:noProxyAnnotationsOnPrivateMethods:@Transactional,@Cacheable,@CacheEvict,@CachePut, and@Asyncmust not be placed onprivatemethods — Spring proxy cannot intercept private methods, so the annotation is silently ignored.
CodeGuard:entityId: Every@Entityclass (or one of its parents within the same codebase) must declare a field annotated with@Id, as JPA requires a primary key to track entity identity.CodeGuard:noDataClassEntity:@Entityclasses must not be Kotlindata classtypes — thefinalmodifier prevents JPA lazy-loading proxies, and structuralequals/hashCodebreaks entity identity semantics.CodeGuard:transactionalPlacement:@Transactionalmust not appear on@Controlleror@RestControllerclasses or their methods; transaction boundaries belong in the service layer.CodeGuard:domainLayerIndependence: Classes residing in..domain..or..entity..packages must not import or use anyorg.springframework.*types, keeping the domain model framework-agnostic and portable.
CodeGuard:serviceNaming: Classes annotated with@Servicemust have names ending inService.CodeGuard:repositoryNaming: Classes and interfaces annotated with@Repositorymust have names ending inRepository.CodeGuard:controllerNaming: Classes annotated with@Controlleror@RestControllermust have names ending inController.CodeGuard:exceptionHandlerNaming: Classes annotated with@RestControllerAdviceor@ControllerAdvicemust have names ending inExceptionHandlerorAdvice.CodeGuard:configurationPropertiesNaming: Classes annotated with@ConfigurationPropertiesmust have names ending inProperties.
CodeGuard:packageNaming: All package names must be fully lowercase — mixed-case packages break Java/Kotlin conventions and cause issues with tooling.CodeGuard:servicePackage:@Serviceclasses must reside in a..service..package segment.CodeGuard:controllerPackage:@Controllerand@RestControllerclasses must reside in a..controller..or..web..package segment.CodeGuard:configurationPackage:@Configurationclasses must reside in a..config..or..configuration..package segment.CodeGuard:propertiesValidation:@ConfigurationPropertiesclasses must reside in a..property..package segment.CodeGuard:configurationPropertiesPrefixKebabCase:@ConfigurationPropertiesprefixes must use lowercase kebab-case segments separated by dots, such asapp.mailorapp-mail.client.CodeGuard:entityPackage:@Entityclasses must reside in a..domain..or..entity..package segment.CodeGuard:onlyServicesInServicePackage: Only@Serviceclasses may reside in a..service..package segment — other stereotypes (@Component,@Repository, etc.) and plain classes must not be placed there. Exception: non-@Serviceclasses declared in the same file as a@Serviceclass are treated as file-level helpers and are permitted; nested helper classes are ignored.CodeGuard:onlyEntitiesInEntityPackage: Only@Entityclasses may reside in..domain..or..entity..package segments — non-entity stereotypes and plain classes must not be placed there. Exception: JPA@MappedSuperclassand@Embeddabletypes are also permitted, classes referenced from an entity's@IdClassare permitted, and non-@Entityclasses declared in the same file as an@Entityclass or a class inheriting from@Entityare treated as file-level helpers and are permitted.CodeGuard:onlyControllersInControllerPackage: Only@Controller/@RestControllerclasses may reside in..controller..or..web..package segments — other stereotypes and plain classes must not be placed there. Exception: non-controller classes declared in the same file as a controller are treated as file-level helpers and are permitted.CodeGuard:onlyConfigurationsInConfigPackage: Only@Configuration,@ControllerAdvice, and@RestControllerAdviceclasses may reside in..config..or..configuration..package segments — other stereotypes and plain classes must not be placed there. Exception: other classes declared in the same file as one of these allowed types are treated as file-level helpers and are permitted.CodeGuard:onlyPropertiesInPropertyPackage: Only@ConfigurationPropertiesclasses may reside in..property..package segments — other stereotypes and plain classes must not be placed there. Exception: non-@ConfigurationPropertiesclasses declared in the same file as a@ConfigurationPropertiesclass are treated as file-level helpers and are permitted.CodeGuard:repositoryPackage:@Repositoryclasses and Spring Data repository interfaces (e.g. extendingJpaRepository) must reside in a..repository..package segment.CodeGuard:onlyRepositoriesInRepositoryPackage: Only@Repositoryclasses may reside in..repository..package segments — other stereotypes and plain classes must not be placed there. Exception: non-@Repositoryclasses declared in the same file as a@Repositoryclass are treated as file-level helpers and are permitted.
CodeGuard:httpMethodAnnotation: Methods in@RestControllerclasses must use specific HTTP method annotations (@GetMapping,@PostMapping,@PutMapping, etc.) instead of the generic@RequestMapping, which requires an explicit method attribute to be unambiguous.CodeGuard:noTrailingSlash: URL path values in HTTP mapping annotations must not end with a trailing slash — inconsistent paths lead to unexpected 404s and routing ambiguity.CodeGuard:restControllerReturnType: GET handler methods in@RestControllerclasses must not returnUnit/void; every GET endpoint must produce a meaningful response body.CodeGuard:dtoSeparation:@RestControllermethods must not accept or return@Entityclasses directly — use dedicated DTOs to decouple the API contract from the persistence model.CodeGuard:controllerRepository:@Controllerand@RestControllerclasses must not declare repository types as constructor parameters or properties; controllers should depend only on services, leaving data access to the service layer.