NOW LET US – AI RAG SaaS Studio TP.HCM
NOW LET US
Digital Product Studio
Back to news
DEV-TOOLS...3 min read

Spring Boot Done Right: Lessons from a 400-Module Codebase

Share
NOW LET US Article – Spring Boot Done Right: Lessons from a 400-Module Codebase

Apereo CAS is one of the largest open-source Spring Boot applications in production. Seven battle-tested patterns from its codebase that will improve yours.

Apereo CAS is one of the largest open-source Spring Boot applications in production. Seven battle-tested patterns from its codebase that will improve yours.

Most Spring Boot tutorials show you a controller, a service, a repository, and call it a day. That’s fine for a TODO app. But what happens when your application grows to 400 modules, gets deployed at thousands of organizations worldwide, and needs to let operators swap out nearly any component without touching your source code?

That’s the problem Apereo CAS solves every day. CAS — the Central Authentication Service — is an identity and single sign-on platform that’s been running in production for over 20 years. Its current incarnation is a Spring Boot 3.x application on Java 21+, and its codebase is one of the best real-world examples I’ve seen of Spring Boot engineering at scale.

I’ve been working in this codebase for years. Here are seven patterns I think every Spring Boot developer should steal.

1. The Thin Auto-Configuration Wrapper

Most Spring Boot projects dump all their bean definitions into a single @Configuration class and call it their auto-configuration. CAS takes a different approach: every auto-configuration class is essentially empty.

support/cas-server-support-simple-mfa/src/main/java/org/apereo/cas/config/CasSimpleMultifactorAuthenticationAutoConfiguration.java

@EnableConfigurationProperties(CasConfigurationProperties.class)
@EnableScheduling
@ConditionalOnFeatureEnabled(feature = CasFeatureModule.FeatureCatalog.SimpleMFA)
@AutoConfiguration
@Import({
CasSimpleMultifactorAuthenticationComponentSerializationConfiguration.class,
CasSimpleMultifactorAuthenticationConfiguration.class,
CasSimpleMultifactorAuthenticationEventExecutionPlanConfiguration.class,
CasSimpleMultifactorAuthenticationMultifactorProviderBypassConfiguration.class,
CasSimpleMultifactorAuthenticationRestConfiguration.class,
CasSimpleMultifactorAuthenticationTicketCatalogConfiguration.class,
CasSimpleMultifactorAuthenticationWebflowConfiguration.class
})
public class CasSimpleMultifactorAuthenticationAutoConfiguration {
}

Empty body. Zero bean definitions. The class exists only to carry annotations — specifically @ConditionalOnFeatureEnabled (should this module load?) and @Import (what configuration classes should load when it does?).

This separation is subtle but powerful. The conditional logic — should this load? — lives in one place. The bean definitions — what should load? — live in the imported configuration classes. You can test the configuration classes independently. You can reuse them across different auto-configuration entry points. And when you’re debugging why a feature didn’t activate, you only need to look at the thin wrapper, not wade through 200 lines of bean definitions to find the one @Conditional annotation that blocked everything.

CAS has 272 of these auto-configuration entry points. Every single one follows this pattern.

2. Building a Custom Feature Flag System on Spring’s @Conditional

Spring Boot gives you @ConditionalOnProperty, @ConditionalOnClass, @ConditionalOnBean. They're useful but generic. CAS needed something more domain-specific: the ability to enable or disable entire subsystems with a single property.

core/cas-server-core-util-api/src/main/java/org/apereo/cas/util/spring/boot/ConditionalOnFeatureEnabled.java

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@Conditional(CasFeatureEnabledCondition.class)
public @interface ConditionalOnFeatureEnabled {
CasFeatureModule.FeatureCatalog[] feature();
String module() default StringUtils.EMPTY;
boolean enabledByDefault() default true;
}

The magic is in @Conditional(CasFeatureEnabledCondition.class) — this annotation delegates to a custom SpringBootCondition implementation that evaluates at startup.

The takeaway isn’t “build your own conditional annotation.” It’s that Spring’s @Conditional mechanism is an extension point, not just a set of built-in annotations. If your domain has a concept that maps to "should this load?" — feature flags, licensing tiers, deployment profiles — you can express it as a type-safe annotation instead of scattering @ConditionalOnProperty checks with magic strings across your codebase.

3. Every Bean is Replaceable

This pattern shows up on virtually every @Bean method in CAS:

@Bean
@ConditionalOnMissingBean(name = "ticketRegistry")
public TicketRegistry ticketRegistry(...) {
return new DefaultTicketRegistry(...);
}

The default in-memory ticket registry only gets created if no other bean named ticketRegistry already exists. When you add cas-server-support-redis-ticket-registry to your classpath, its auto-configuration creates a RedisTicketRegistry bean with that same name — and the default one quietly steps aside.

This means operators can override virtually any component in CAS by defining their own bean with the right name. No subclassing, no framework hooks, no BeanDefinitionRegistryPostProcessor gymnastics. Just standard Spring.

4. The Execution Plan Configurer Pattern

Here’s a problem CAS faces constantly: multiple independent modules need to contribute to a single capability. The LDAP module registers an authentication handler. The JDBC module registers a different one. The YubiKey module registers an MFA handler. None of them know about each other, and they all need to plug into the same authentication pipeline.

CAS solves this with what I call the “configurer pattern.” First, there’s a plan interface — a registry that accepts contributions:

public interface AuthenticationEventExecutionPlan {
boolean registerAuthenticationHandler(AuthenticationHandler handler);
void registerAuthenticationMetadataPopulator(AuthenticationMetaDataPopulator populator);
void registerAuthenticationPolicy(AuthenticationPolicy authenticationPolicy);
}
© 2026 Now Let Us. All rights reserved.

Source: Hacker News

Advertisement
Ad slot ready: 5887729102

More in this category

EXPLORE TOPICS

Discover All Categories

Deep dive into the specific technology sectors that matter most to you.