I Built a Production-Ready Spring Boot 4.1.0 SaaS Boilerplate — Here Is What I Learned
> Why I Built This
Every Spring Boot project I started, I spent the first **2–3 weeks** building the exact same things:
* JWT authentication
* Email verification
* Forgot password flow
* Google OAuth2 integration
* Docker setup
* CI/CD pipeline
That is weeks of work before writing a single line of my actual product.
So I packaged all of it into **SpringLaunch API** — a production-ready Spring Boot **4.1.0** boilerplate.
Here are the biggest lessons I learned while building it.
## Spring Boot 4 Changed A Lot
Spring Boot **4.1.0** (released June 2026) is a major release.
Several APIs changed compared to Boot 3.
## Test packages moved
// Spring Boot 3
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
// Spring Boot 4
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
## `@MockBean` became `@MockitoBean`
// Spring Boot 3
@MockBean
private JwtService jwtService;
// Spring Boot 4
@MockitoBean
private JwtService jwtService;
## `DaoAuthenticationProvider` is now auto-configured
If your application exposes both:
* `UserDetailsService`
* `PasswordEncoder`
Spring Security automatically creates the `DaoAuthenticationProvider`.
No manual bean configuration is required anymore.
## JWT Strategy — Two Tokens, Two Places
I use a hybrid authentication strategy.
Token | Lifetime | Storage
---|---|---
Access Token | 15 minutes | JSON response body
Refresh Token | 7 days | HTTP-only Cookie
Access token:
public record AuthResponse(
String accessToken,
UserResponse user
) {}
Refresh token:
ResponseCookie cookie = ResponseCookie.from("refreshToken", token)
.httpOnly(true)
.secure(true)
.sameSite("Lax")
.path("/")
.maxAge(maxAgeSeconds)
.build();
### Why HTTP-only cookies?
JavaScript cannot read them.
Even if an XSS attack executes on the page, it cannot steal the refresh token.
## The `@Async` Self-Invocation Trap
This bug cost me over an hour.
Calling an `@Async` method from the same class bypasses Spring's proxy, so the method executes **synchronously**.
❌ Wrong
@Service
public class AuthServiceImpl {
@Async
private void sendEmail() { }
public void register() {
sendEmail();
}
}
✅ Correct
@Service
public class EmailService {
@Async
public void sendEmail() { }
}
@Service
public class AuthServiceImpl {
private final EmailService emailService;
public void register() {
emailService.sendEmail();
}
}
Because the call goes through Spring's proxy, it becomes truly asynchronous.
## Argon2 — Use Password4j
Spring Security's `Argon2PasswordEncoder` is soft-deprecated.
Instead, Boot 4 recommends Password4j integration.
❌ Old
new Argon2PasswordEncoder(
16,
32,
1,
65536,
3
);
✅ Recommended
import org.springframework.security.crypto.password4j.Argon2Password4jPasswordEncoder;
new Argon2Password4jPasswordEncoder();
Cleaner code with recommended defaults.
## Factory Methods On JPA Entities
JPA entities cannot be records because they require:
* mutable fields
* a no-argument constructor
Instead, I use factory methods.
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User extends BaseEntity implements UserDetails {
public static User ofLocal(
String name,
String username,
String email,
String encodedPassword
) {
User user = new User();
user.name = name.trim();
user.username = username.toLowerCase();
user.email = email.toLowerCase();
user.password = encodedPassword;
user.role = UserRole.USER;
user.provider = AuthProvider.LOCAL;
user.emailVerified = false;
return user;
}
public static User ofGoogle(
String name,
String username,
String email,
String providerId
) {
User user = new User();
// ...
user.emailVerified = true;
return user;
}
}
No one can accidentally create an invalid user.
## API Versioning From Day One
Every endpoint is versioned.
public final class ApiVersion {
public static final String V1 = "/v1";
private ApiVersion() {}
}
Controllers simply use:
@RequestMapping(ApiVersion.V1 + "/auth")
public class AuthController {
}
When breaking changes arrive:
* Add `/v2`
* Keep `/v1`
* Existing clients never break
## What Is Included
SpringLaunch API currently includes:
* Spring Boot 4.1.0
* Java 21
* JWT Authentication
* Google OAuth2 Login
* Email Verification
* Password Reset
* 17 REST Endpoints
* 42 Automated Tests
* Docker Compose
* GitHub Actions CI
* Render Deployment Configuration
* 7 Documentation Guides
## Final Thoughts
Building this taught me far more than just authentication.
I learned how Spring Boot 4 changed testing, security, password encoding, asynchronous execution, and application architecture.
Most importantly, I now have a production-ready starting point that saves **weeks** every time I build a new SaaS.
If you're interested, you can check out **SpringLaunch API** here:
**Landing Page**
https://sujankim.github.io/springlaunch
It's also available on Gumroad.
I'd love to hear your thoughts or answer any questions about the implementation decisions.