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/C
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:
UserDetailsServicePasswordEncoder
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.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.