Fetching latest headlines…
I Built a Production-Ready Spring Boot 4.1.0 SaaS Boilerplate — Here Is What I Learned
NORTH AMERICA
🇺🇸 United StatesJuly 5, 2026

I Built a Production-Ready Spring Boot 4.1.0 SaaS Boilerplate — Here Is What I Learned

0 views0 likes0 comments
Originally published byDev.to

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.

Comments (0)

Sign in to join the discussion

Be the first to comment!