This document describes the Spring Boot architecture of Source Code Portal, including initialization flow, bean configuration, and Spring integration patterns.
- Overview
- Application Initialization
- Spring Bean Configuration
- Configuration Management
- Actuator Integration
- Scheduled Tasks
- Testing Infrastructure
- Migration from Legacy Mode
Source Code Portal uses Spring Boot 3.2.2 as its primary application framework. Spring Boot provides:
- Dependency Injection: Constructor-based DI throughout the application
- Auto-Configuration: Automatic setup of common infrastructure
- Actuator Endpoints: Production-ready monitoring and management
- Configuration Management: Type-safe, hierarchical configuration
- Testing Support: Comprehensive test infrastructure
The migration to Spring Boot (Phase 2) brought several benefits:
- Industry Standard: Well-known patterns and practices
- Better Testability: Spring Test framework integration
- Observability: Built-in health checks and metrics
- Developer Experience: Better IDE support and tooling
- Maintainability: Less boilerplate, cleaner code
The application starts via SpringBootServer.main() and follows this initialization sequence:
1. Load Configuration
├─ application.yml (defaults)
├─ application-defaults.properties (legacy)
├─ security.properties (credentials)
├─ application_override.properties (overrides)
├─ Environment variables (SCP_* prefix)
└─ System properties
2. Initialize Spring Context
├─ Component scanning
├─ Auto-configuration
└─ Bean creation
3. Create Core Beans
├─ CacheStore (Caffeine + Spring Cache)
├─ ExecutorService (virtual threads)
├─ ScheduledExecutorService
└─ RepositoryConfigLoader
4. Run Startup Initializer
├─ Load config.json
├─ Fetch GitHub repository list
├─ Validate configuration
└─ Log system status
5. Prefetch Data
├─ Load repository metadata
├─ Fetch recent commits
├─ Cache documentation
└─ Populate build status
6. Start Embedded Server
├─ Initialize Undertow
├─ Register Spring MVC handlers
├─ Start listening on port 9090
└─ Log ready message
File: src/main/java/no/cantara/docsite/SpringBootServer.java
@SpringBootApplication
@EnableCaching
@EnableScheduling
public class SpringBootServer {
private static final Logger log = LoggerFactory.getLogger(SpringBootServer.class);
public static void main(String[] args) {
// Set system properties for virtual threads
System.setProperty("jdk.virtualThreadScheduler.parallelism", "10");
System.setProperty("jdk.virtualThreadScheduler.maxPoolSize", "10");
// Launch Spring Boot
SpringApplication.run(SpringBootServer.class, args);
log.info("Source Code Portal started successfully");
}
}Phase 1: Configuration Loading
Spring Boot loads configuration in this order (later overrides earlier):
application.yml- Main configurationapplication-defaults.properties- Legacy defaultssecurity.properties- Credentials (excluded from Git)application_override.properties- Local overrides- Environment variables with
SCP_prefix - Command-line arguments
Phase 2: Component Scanning
Spring scans these packages for components:
no.cantara.docsite.controller.spring- Spring MVC controllersno.cantara.docsite.config- Configuration classesno.cantara.docsite.actuator- Custom health indicatorsno.cantara.docsite.scheduled- Scheduled tasks
Phase 3: Auto-Configuration
Spring Boot auto-configures:
- Embedded Undertow server
- Spring MVC (DispatcherServlet)
- Thymeleaf template engine
- Spring Cache (Caffeine)
- Jackson JSON serialization
- Actuator endpoints
File: src/main/java/no/cantara/docsite/config/CacheConfiguration.java
@Configuration
@EnableCaching
public class CacheConfiguration {
@Bean
public CacheStore cacheStore(CacheManager cacheManager) {
return new CacheStore(cacheManager);
}
@Bean
public CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
// Configure individual caches with Caffeine
cacheManager.setCaches(Arrays.asList(
buildCache("repositories"),
buildCache("commits"),
buildCache("contents"),
buildCache("buildStatus"),
buildCache("badges")
));
return cacheManager;
}
private CaffeineCache buildCache(String name) {
return new CaffeineCache(name, Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(15))
.recordStats()
.build());
}
}File: src/main/java/no/cantara/docsite/config/ExecutorConfiguration.java
@Configuration
public class ExecutorConfiguration {
@Bean
public ExecutorService executorService() {
// Use virtual threads for I/O-bound tasks
return Executors.newVirtualThreadPerTaskExecutor();
}
@Bean
public ScheduledExecutorService scheduledExecutorService() {
return Executors.newScheduledThreadPool(
5, // Core pool size
Thread.ofVirtual().factory()
);
}
}File: src/main/java/no/cantara/docsite/config/ApplicationProperties.java
Type-safe configuration properties:
@Configuration
@ConfigurationProperties(prefix = "scp")
public class ApplicationProperties {
private Server server = new Server();
private Github github = new Github();
private Cache cache = new Cache();
public static class Server {
private String mode = "spring-boot";
private int port = 9090;
private String contextPath = "/";
// getters/setters
}
public static class Github {
private String accessToken;
private String organization;
private String visibility = "public";
// getters/setters
}
public static class Cache {
private boolean enabled = true;
private Duration ttl = Duration.ofMinutes(15);
// getters/setters
}
}File: src/main/java/no/cantara/docsite/config/WebMvcConfiguration.java
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*");
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// Serve static resources
registry.addResourceHandler("/css/**")
.addResourceLocations("classpath:/META-INF/views/css/");
registry.addResourceHandler("/js/**")
.addResourceLocations("classpath:/META-INF/views/js/");
registry.addResourceHandler("/images/**")
.addResourceLocations("classpath:/META-INF/views/images/");
}
}Creation Order:
- Configuration classes (
@Configuration) - Property binding (
@ConfigurationProperties) - Infrastructure beans (CacheManager, ExecutorService)
- Domain services (RepositoryConfigLoader, CacheStore)
- Controllers (
@RestController,@Controller) - Actuator components (health indicators, info contributors)
Initialization Callbacks:
@PostConstruct- Bean initializationApplicationRunner- Post-startup tasksSmartLifecycle- Advanced lifecycle management
1. application.yml (Primary)
scp:
server:
mode: spring-boot
port: 9090
github:
organization: cantara
visibility: public
api:
timeout: 75s
rate-limit-buffer: 100
cache:
enabled: true
ttl: 15m
max-size: 1000
spring:
application:
name: source-code-portal
cache:
type: caffeine
caffeine:
spec: maximumSize=1000,expireAfterWrite=15m2. Legacy Properties (Backward Compatibility)
The ConfigurationBridge class provides backward compatibility with legacy property files:
@Component
public class ConfigurationBridge {
@Autowired
private Environment environment;
/**
* Get property with fallback to legacy property names.
*/
public String getProperty(String key) {
// Try new Spring Boot property first
String value = environment.getProperty(key);
// Fall back to legacy property name
if (value == null) {
String legacyKey = toLegacyKey(key);
value = environment.getProperty(legacyKey);
}
return value;
}
private String toLegacyKey(String springBootKey) {
// Convert: scp.github.access-token -> github.client.accessToken
return springBootKey.replace("scp.", "").replace("-", ".");
}
}All configuration can be overridden via environment variables with SCP_ prefix:
# Override GitHub access token
export SCP_GITHUB_ACCESS_TOKEN=ghp_xxxxx
# Override server port
export SCP_SERVER_PORT=8080
# Override cache TTL
export SCP_CACHE_TTL=30mNaming Convention:
scp.github.access-token→SCP_GITHUB_ACCESS_TOKENscp.cache.ttl→SCP_CACHE_TTL- Dots become underscores, kebab-case becomes UPPER_SNAKE_CASE
Development Profile:
# application-dev.yml
scp:
cache:
enabled: false # Disable caching for live data
github:
api:
timeout: 30s # Faster timeouts for dev
logging:
level:
no.cantara.docsite: DEBUGProduction Profile:
# application-prod.yml
scp:
cache:
ttl: 30m # Longer cache for production
github:
api:
timeout: 75s
logging:
level:
no.cantara.docsite: INFOActivate Profile:
mvn spring-boot:run -Dspring-boot.run.profiles=dev
# or
java -jar app.jar --spring.profiles.active=prodHealth Checks:
/actuator/health- Overall application health/actuator/health/github- GitHub API rate limit status/actuator/health/cache- Cache health and statistics/actuator/health/executor- Thread pool health
Metrics:
/actuator/metrics- All available metrics/actuator/metrics/jvm.memory.used- JVM memory usage/actuator/metrics/http.server.requests- HTTP request metrics/actuator/prometheus- Prometheus format metrics
Application Info:
/actuator/info- Application version, build info, runtime details/actuator/caches- Cache manager details/actuator/scheduledtasks- Scheduled task list
@Component
public class GitHubHealthIndicator implements HealthIndicator {
private final GitHubCommands gitHubCommands;
@Override
public Health health() {
try {
RateLimit rateLimit = gitHubCommands.getRateLimit();
Map<String, Object> details = Map.of(
"remaining", rateLimit.remaining(),
"limit", rateLimit.limit(),
"reset", rateLimit.reset()
);
if (rateLimit.remaining() < 100) {
return Health.down()
.withDetails(details)
.build();
}
return Health.up()
.withDetails(details)
.build();
} catch (Exception e) {
return Health.down()
.withException(e)
.build();
}
}
}@Component
public class CacheHealthIndicator implements HealthIndicator {
private final CacheStore cacheStore;
@Override
public Health health() {
Map<String, Object> details = new HashMap<>();
for (String cacheName : cacheStore.getCacheNames()) {
CaffeineCache cache = (CaffeineCache) cacheStore.getCache(cacheName);
CacheStats stats = cache.getNativeCache().stats();
details.put(cacheName, Map.of(
"size", cache.getNativeCache().estimatedSize(),
"hitRate", stats.hitRate(),
"missRate", stats.missRate()
));
}
return Health.up()
.withDetails(details)
.build();
}
}@Component
public class ApplicationInfoContributor implements InfoContributor {
@Value("${scp.version:unknown}")
private String version;
@Autowired
private ApplicationProperties properties;
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("app", Map.of(
"version", version,
"mode", properties.getServer().getMode(),
"organization", properties.getGithub().getOrganization()
));
builder.withDetail("runtime", Map.of(
"javaVersion", System.getProperty("java.version"),
"processors", Runtime.getRuntime().availableProcessors(),
"maxMemory", Runtime.getRuntime().maxMemory()
));
}
}Spring Boot replaces custom executor-based scheduling with declarative @Scheduled annotations:
@Component
public class ScheduledFetchData {
private final CacheStore cacheStore;
private final RepositoryConfigLoader configLoader;
@Scheduled(
fixedDelayString = "${scp.fetch.interval:PT15M}",
initialDelayString = "${scp.fetch.initial-delay:PT1M}"
)
public void refreshRepositories() {
log.info("Starting scheduled repository refresh");
List<Repository> repos = configLoader.getAllRepositories();
for (Repository repo : repos) {
try {
fetchAndCache(repo);
} catch (Exception e) {
log.error("Failed to fetch repository: {}", repo.getName(), e);
}
}
log.info("Completed repository refresh");
}
@Scheduled(cron = "${scp.fetch.commits.cron:0 */30 * * * *}")
public void refreshCommits() {
log.info("Starting scheduled commit refresh");
// Refresh commit data
}
}Configuration:
scp:
fetch:
interval: PT15M # ISO-8601 duration (15 minutes)
initial-delay: PT1M
commits:
cron: "0 */30 * * * *" # Every 30 minutesView all scheduled tasks:
curl http://localhost:9090/actuator/scheduledtasksResponse:
{
"cron": [],
"fixedDelay": [
{
"runnable": {
"target": "no.cantara.docsite.scheduled.ScheduledFetchData.refreshRepositories"
},
"initialDelay": 60000,
"interval": 900000
}
],
"fixedRate": []
}Test Configuration:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(properties = {
"scp.github.access-token=test-token",
"scp.cache.enabled=false"
})
public class SpringBootServerTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
public void testHealthEndpoint() {
ResponseEntity<String> response = restTemplate.getForEntity(
"/actuator/health",
String.class
);
assertEquals(HttpStatus.OK, response.getStatusCode());
}
}@WebMvcTest for Controllers:
@WebMvcTest(DashboardWebController.class)
public class DashboardWebControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private CacheStore cacheStore;
@Test
public void testDashboardPage() throws Exception {
mockMvc.perform(get("/dashboard"))
.andExpect(status().isOk())
.andExpect(view().name("index"))
.andExpect(model().attributeExists("groups"));
}
}TestServerExtension (JUnit 5):
@ExtendWith(TestServerExtension.class)
public class IntegrationTest {
@Test
public void testFullFlow(TestClient client) {
String response = client.get("/dashboard");
assertTrue(response.contains("Source Code Portal"));
}
}The application supports both Spring Boot and legacy Undertow modes during the transition period:
Mode Selection:
scp:
server:
mode: spring-boot # or "undertow"Entry Points:
- Spring Boot:
SpringBootServer.main() - Legacy Undertow:
Server.main()
- v0.10.17: Spring Boot mode introduced (Phase 2 complete)
- v0.11.0: Legacy mode marked deprecated
- v1.0.0: Legacy mode removed (planned)
For teams migrating from legacy to Spring Boot mode:
- Update startup scripts to use
SpringBootServer - Migrate configuration to
application.yml - Update monitoring to use Actuator endpoints
- Test all endpoints with Spring MVC controllers
- Update health check URLs (
/actuator/health) - Verify cache configuration with Caffeine
- Test webhook integration
- Update Docker deployment
- Controller Architecture - Request flow and Spring MVC
- Caching Architecture - Cache configuration and strategies
- Configuration Guide - Detailed configuration reference
- Monitoring Guide - Actuator and observability
Next Steps: Read the Controller Architecture document to understand request routing and Spring MVC integration.