{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreidmohxelpwsmybvrkms3i6sllxjrwsgozvu6gkyhxw6wxewpvq3fi",
    "uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mpvjsgnq2er2"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreibdddtffovxonbdyn6dyqmb4yv6gntpagystoamiql5rd5uc5odpm"
    },
    "mimeType": "image/webp",
    "size": 533188
  },
  "path": "/sujankim/i-built-a-production-ready-spring-boot-410-saas-boilerplate-here-is-what-i-learned-2164",
  "publishedAt": "2026-07-05T11:18:58.000Z",
  "site": "https://dev.to",
  "tags": [
    "springboot",
    "java",
    "webdev",
    "opensource",
    "https://sujankim.github.io/springlaunch",
    "@MockBean",
    "@MockitoBean",
    "@Async",
    "@Service",
    "@Entity",
    "@NoArgsConstructor",
    "@RequestMapping"
  ],
  "textContent": "> Why I Built This\n\nEvery Spring Boot project I started, I spent the first **2–3 weeks** building the exact same things:\n\n  * JWT authentication\n  * Email verification\n  * Forgot password flow\n  * Google OAuth2 integration\n  * Docker setup\n  * CI/CD pipeline\n\n\n\nThat is weeks of work before writing a single line of my actual product.\n\nSo I packaged all of it into **SpringLaunch API** — a production-ready Spring Boot **4.1.0** boilerplate.\n\nHere are the biggest lessons I learned while building it.\n\n##  Spring Boot 4 Changed A Lot\n\nSpring Boot **4.1.0** (released June 2026) is a major release.\n\nSeveral APIs changed compared to Boot 3.\n\n##  Test packages moved\n\n\n    // Spring Boot 3\n    import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;\n\n    // Spring Boot 4\n    import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;\n\n\n##  `@MockBean` became `@MockitoBean`\n\n\n    // Spring Boot 3\n    @MockBean\n    private JwtService jwtService;\n\n    // Spring Boot 4\n    @MockitoBean\n    private JwtService jwtService;\n\n\n##  `DaoAuthenticationProvider` is now auto-configured\n\nIf your application exposes both:\n\n  * `UserDetailsService`\n  * `PasswordEncoder`\n\n\n\nSpring Security automatically creates the `DaoAuthenticationProvider`.\n\nNo manual bean configuration is required anymore.\n\n##  JWT Strategy — Two Tokens, Two Places\n\nI use a hybrid authentication strategy.\n\nToken | Lifetime | Storage\n---|---|---\nAccess Token | 15 minutes | JSON response body\nRefresh Token | 7 days | HTTP-only Cookie\n\nAccess token:\n\n\n\n    public record AuthResponse(\n        String accessToken,\n        UserResponse user\n    ) {}\n\n\nRefresh token:\n\n\n\n    ResponseCookie cookie = ResponseCookie.from(\"refreshToken\", token)\n        .httpOnly(true)\n        .secure(true)\n        .sameSite(\"Lax\")\n        .path(\"/\")\n        .maxAge(maxAgeSeconds)\n        .build();\n\n\n###  Why HTTP-only cookies?\n\nJavaScript cannot read them.\n\nEven if an XSS attack executes on the page, it cannot steal the refresh token.\n\n##  The `@Async` Self-Invocation Trap\n\nThis bug cost me over an hour.\n\nCalling an `@Async` method from the same class bypasses Spring's proxy, so the method executes **synchronously**.\n\n❌ Wrong\n\n\n\n    @Service\n    public class AuthServiceImpl {\n\n        @Async\n        private void sendEmail() { }\n\n        public void register() {\n            sendEmail();\n        }\n    }\n\n\n✅ Correct\n\n\n\n    @Service\n    public class EmailService {\n\n        @Async\n        public void sendEmail() { }\n    }\n\n    @Service\n    public class AuthServiceImpl {\n\n        private final EmailService emailService;\n\n        public void register() {\n            emailService.sendEmail();\n        }\n    }\n\n\nBecause the call goes through Spring's proxy, it becomes truly asynchronous.\n\n##  Argon2 — Use Password4j\n\nSpring Security's `Argon2PasswordEncoder` is soft-deprecated.\n\nInstead, Boot 4 recommends Password4j integration.\n\n❌ Old\n\n\n\n    new Argon2PasswordEncoder(\n        16,\n        32,\n        1,\n        65536,\n        3\n    );\n\n\n✅ Recommended\n\n\n\n    import org.springframework.security.crypto.password4j.Argon2Password4jPasswordEncoder;\n\n    new Argon2Password4jPasswordEncoder();\n\n\nCleaner code with recommended defaults.\n\n##  Factory Methods On JPA Entities\n\nJPA entities cannot be records because they require:\n\n  * mutable fields\n  * a no-argument constructor\n\n\n\nInstead, I use factory methods.\n\n\n\n    @Entity\n    @NoArgsConstructor(access = AccessLevel.PROTECTED)\n    public class User extends BaseEntity implements UserDetails {\n\n        public static User ofLocal(\n            String name,\n            String username,\n            String email,\n            String encodedPassword\n        ) {\n\n            User user = new User();\n\n            user.name = name.trim();\n            user.username = username.toLowerCase();\n            user.email = email.toLowerCase();\n            user.password = encodedPassword;\n\n            user.role = UserRole.USER;\n            user.provider = AuthProvider.LOCAL;\n            user.emailVerified = false;\n\n            return user;\n        }\n\n        public static User ofGoogle(\n            String name,\n            String username,\n            String email,\n            String providerId\n        ) {\n\n            User user = new User();\n\n            // ...\n\n            user.emailVerified = true;\n\n            return user;\n        }\n    }\n\n\nNo one can accidentally create an invalid user.\n\n##  API Versioning From Day One\n\nEvery endpoint is versioned.\n\n\n\n    public final class ApiVersion {\n\n        public static final String V1 = \"/v1\";\n\n        private ApiVersion() {}\n    }\n\n\nControllers simply use:\n\n\n\n    @RequestMapping(ApiVersion.V1 + \"/auth\")\n    public class AuthController {\n    }\n\n\nWhen breaking changes arrive:\n\n  * Add `/v2`\n  * Keep `/v1`\n  * Existing clients never break\n\n\n\n##  What Is Included\n\nSpringLaunch API currently includes:\n\n  * Spring Boot 4.1.0\n  * Java 21\n  * JWT Authentication\n  * Google OAuth2 Login\n  * Email Verification\n  * Password Reset\n  * 17 REST Endpoints\n  * 42 Automated Tests\n  * Docker Compose\n  * GitHub Actions CI\n  * Render Deployment Configuration\n  * 7 Documentation Guides\n\n\n\n##  Final Thoughts\n\nBuilding this taught me far more than just authentication.\n\nI learned how Spring Boot 4 changed testing, security, password encoding, asynchronous execution, and application architecture.\n\nMost importantly, I now have a production-ready starting point that saves **weeks** every time I build a new SaaS.\n\nIf you're interested, you can check out **SpringLaunch API** here:\n\n**Landing Page**\n\nhttps://sujankim.github.io/springlaunch\n\nIt's also available on Gumroad.\n\nI'd love to hear your thoughts or answer any questions about the implementation decisions.",
  "title": "I Built a Production-Ready Spring Boot 4.1.0 SaaS Boilerplate — Here Is What I Learned"
}