Canonical reference for Spring Boot idioms, patterns, and code quality standards used across this project.
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
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
}@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>"
}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 { ... }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=actuatorProfile-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.
- 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> { ... }
}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
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`() { ... }
}| 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 |