Spring Guidelines
July 6, 2026 · View on GitHub
Canonical reference for Spring Boot idioms, patterns, and code quality standards used across this project.
Bean Registration
Register beans explicitly in @Configuration classes using @Bean factory methods — not via component scanning or class-level @Service/@Component annotations.
@Configuration
class ServiceConfiguration {
@Bean
fun bucketService(bucketStore: BucketStore, objectStore: ObjectStore): BucketService =
BucketService(bucketStore, objectStore)
}
Three configuration layers mirror the architecture:
StoreConfiguration— store beansServiceConfiguration— service beans (imported in@SpringBootTest)ControllerConfiguration— controller, filter, and exception-handler beans
Dependency Injection
Always use constructor injection. Never use @Autowired on fields or setters in production code.
// DO — constructor injection
class BucketService(
private val bucketStore: BucketStore,
private val objectStore: ObjectStore,
)
// DON'T — field injection
class BucketService {
@Autowired private lateinit var bucketStore: BucketStore
}
Controllers
@RestControllerclasses map HTTP only — never contain business logic- All logic is delegated to a
@Service; controllers call the service and return the result - Return typed DTOs, never raw strings
- Controllers never catch exceptions — exception handlers in
ControllerConfigurationdo that
// DO
@GetMapping("/{bucketName}")
fun getBucket(@PathVariable bucketName: String): ResponseEntity<ListBucketResult> =
ResponseEntity.ok(bucketService.listObjects(bucketName))
// DON'T
@GetMapping("/{bucketName}")
fun getBucket(@PathVariable bucketName: String): String {
// business logic here ...
return "<ListBucketResult>...</ListBucketResult>"
}
Configuration Properties
Bind configuration via @ConfigurationProperties data classes — never inject individual values with @Value in production code.
@JvmRecord
@ConfigurationProperties("com.adobe.testing.s3mock.store")
data class StoreProperties(
@param:DefaultValue("false") val retainFilesOnExit: Boolean,
@param:DefaultValue("us-east-1") val region: Region,
)
Enable each properties class with @EnableConfigurationProperties in the matching @Configuration:
@Configuration
@EnableConfigurationProperties(StoreProperties::class)
class StoreConfiguration { ... }
Profiles
S3Mock uses Spring Boot profile groups to compose behaviour:
| Profile | Activates | Purpose |
|---|---|---|
debug | actuator | Debug-level logging + full actuator |
trace | actuator | Trace-level logging + full actuator |
actuator | — | JMX + all actuator endpoints exposed |
Profile groups are defined in application.properties:
spring.profiles.group.debug=actuator
spring.profiles.group.trace=actuator
Profile-specific properties files:
application-debug.properties— logging levels onlyapplication-trace.properties— logging levels onlyapplication-actuator.properties— JMX and actuator endpoint settings
Actuator endpoints are disabled by default (management.endpoints.access.default=none in application.properties).
The actuator profile overrides this to unrestricted.
Exception Handling
- Services throw
S3Exceptionconstants (e.g.,S3Exception.NO_SUCH_BUCKET) — never create new exception classes - Exception handlers are
@ControllerAdviceclasses registered as@Beans inControllerConfiguration S3MockExceptionHandlerconvertsS3Exception→ XMLErrorResponsewith the correct HTTP statusIllegalStateExceptionHandlerconverts unexpected errors →500 InternalError
@ControllerAdvice
class S3MockExceptionHandler : ResponseEntityExceptionHandler() {
@ExceptionHandler(S3Exception::class)
fun handleS3Exception(s3Exception: S3Exception): ResponseEntity<ErrorResponse> { ... }
}
Testing
Service and Store Tests — @SpringBootTest
Use @SpringBootTest scoped to the relevant @Configuration class with @MockitoBean for dependencies.
@SpringBootTest(classes = [ServiceConfiguration::class], webEnvironment = SpringBootTest.WebEnvironment.NONE)
@MockitoBean(types = [MultipartService::class, MultipartStore::class])
internal class BucketServiceTest : ServiceTestBase() {
@Autowired private lateinit var iut: BucketService
@Test
fun `should return no such bucket`() { ... }
}
Always extend the appropriate base class:
ServiceTestBase— service layer testsStoreTestBase— store layer tests
Controller Tests — @WebMvcTest
Use @WebMvcTest scoped to the controller under test with @MockitoBean for services and BaseControllerTest for shared fixtures.
@WebMvcTest(controllers = [BucketController::class], ...
@MockitoBean(types = [BucketService::class])
internal class BucketControllerTest : BaseControllerTest() {
@Autowired private lateinit var mockMvc: MockMvc
@Autowired private lateinit var bucketService: BucketService
@Test
fun `should list buckets`() { ... }
}
Common Anti-Patterns
The first row is enforced by the noStandardMockitoUnitTests ArchUnit rule (make test) for Spring-managed components; see INVARIANTS.md.
| Anti-Pattern | Refactor To |
|---|---|
@ExtendWith(MockitoExtension::class) + @Mock + @InjectMocks | @SpringBootTest + @MockitoBean + @Autowired |
@Autowired field injection in production code | Constructor injection |
| Business logic in controller method | Delegate to a service class |
Returning a raw String from a controller | Return a typed DTO wrapped in ResponseEntity |
@Value("${property}") scattered throughout beans | @ConfigurationProperties data class |
| New exception class for S3 errors | S3Exception constant |