What Every Java SaaS Backend Keeps Rebuilding - and How to Structure It Once
Starting a new SaaS product is exciting until you realize how much work happens before you can build the feature that actually makes the product unique. For many Java and Spring Boot applications, the list looks familiar: - authentication; - refresh token handling; - organizations and tenants; - authorization; - audit trails; - database migrations; - consistent API errors; - API documentation; - automated tests. None of these features is the reason the SaaS exists. But they quickly become part of its foundation. The interesting question is not whether we need them. It is: How can we structure these recurring concerns so that we don't redesign the foundation every time we start a new product? 1. Authentication is more than issuing a JWT A basic authentication example can be surprisingly small. A production-oriented authentication design usually isn't. Once access tokens are introduced, several other decisions appear: - How are tokens signed and verified? - How long should access tokens live? - How are refresh tokens handled? - Can refresh tokens be rotated? - What happens when an old refresh token is reused? - How do we revoke sessions without turning access-token validation into a database lookup? These decisions should live inside an explicit authentication boundary rather than being scattered across controllers and business services. For example, a SaaS foundation might use asymmetric JWT signing such as RS256 for access tokens while treating refresh tokens as a separate lifecycle. The important part isn't RS256 itself. It's making authentication a well-defined subsystem. 2. Multi-tenancy needs an explicit context For many B2B SaaS applications, authentication answers: Who is this user? But the application also needs to answer: In which organization is this operation happening? That distinction becomes critical when the same user can interact with different organizations. One useful pattern is establishing a tenant or organization context near the request boundary. Conceptually: HTTP Request | Authentication | Organization Resolution | TenantContext | Authorization | Application Service | Persistence Instead of passing an organization identifier manually through every layer, the application establishes the active context and downstream components operate within that boundary. This doesn't magically make an application tenant-safe. Queries, authorization rules, tests, and persistence boundaries still need to enforce isolation. But the context makes the tenant boundary explicit. 3. Roles alone eventually become limiting A simple application might start with: ADMIN USER Then requirements arrive. One user can manage billing but not members. Another can view reports but not modify settings. Someone else can manage operational data without having administrative access. If every combination becomes another role, the model quickly becomes difficult to maintain. A capability-based model gives us another level of abstraction: ROLE | +-- capability:members.read +-- capability:members.write +-- capability:billing.read +-- capability:settings.write Roles can still exist. But instead of business code asking: Is this user an ADMIN? it can ask: Can this user perform this capability? That distinction becomes increasingly valuable as a SaaS product grows. 4. Audit logs are different from application logs Application logs help engineers understand what the software is doing. Audit records answer a different class of question: Who performed this business-relevant action, in which organization, and when? Examples include: - changing a member's permissions; - modifying organization settings; - creating or revoking access; - performing sensitive administrative operations. These events benefit from a structured, append-oriented model. An audit entry might contain information such as: actor organization action resource timestamp metadata The exact model varies by product, but treating auditing as an architectural concern from the beginning is much easier than reconstructing it later from application logs. 5. Database evolution should be reproducible A database schema isn't static. Tables change. Indexes appear. Constraints evolve. New environments need to reproduce the same structure. Database migrations provide an ordered history of that evolution. With PostgreSQL and a migration tool such as Flyway, the application can keep schema changes versioned alongside the codebase. This gives developers a repeatable path from: empty database to: current application schema without relying on undocumented manual SQL steps. 6. API errors deserve a contract too Successful API responses usually receive a lot of design attention. Errors often don't. Without a standard, endpoints gradually return different shapes for validation failures, authorization errors, missing resources, and unexpected conditions. RFC 7807-style problem details provide a useful foundation for a consistent error contract. For example: { "type": "...", "title": "Forbidden", "status": 403, "detail": "...", "instance": "..." } The goal isn't simply compliance with a specification. The goal is predictability for every client consuming the API. 7. Documentation should describe the actual API OpenAPI becomes significantly more useful when it evolves with the application instead of living as a document someone occasionally remembers to update. A useful SaaS foundation should make the API contract discoverable and keep documentation close to the implementation. This becomes particularly valuable when the backend is consumed by: - a web frontend; - mobile applications; - external integrations; - other services. 8. The foundation needs tests Reusable architecture without tests can become reusable uncertainty. The important behaviors of the foundation should be executable. That includes areas such as: - authentication behavior; - authorization boundaries; - organization isolation; - refresh token lifecycle; - persistence behavior; - API contracts. Tests aren't just there to prevent regressions. They document what the foundation promises to do. The recurring pattern Put these pieces together and a familiar architecture begins to emerge: Request | Authentication | Organization Context | Capability Authorization | Application Logic | Persistence | PostgreSQL Surrounding that core are: Audit Migrations OpenAPI Problem Details Automated Tests None of this defines the business domain. And that's exactly the point. These are infrastructure and application concerns that appear repeatedly across SaaS products. The domain should be the part that changes. The foundation shouldn't need to be reinvented every time. Reuse the foundation, not the business There's an important distinction here. A reusable SaaS foundation shouldn't attempt to be a finished SaaS application. Otherwise, developers spend their time removing someone else's product assumptions. A better boundary is: Reusable foundation + Your business domain = Your product A reference application can still be extremely valuable, but its purpose should be to demonstrate how the foundation is used - not to become the template developers blindly copy. Why we built the LDS SaaS Starter This recurring problem is what led us to build the LDS SaaS Starter at LDS Labs. It's a reusable Java 21 + Spring Boot backend foundation where concerns such as authentication, organizational context, capability-based authorization, auditing, PostgreSQL/Flyway persistence, OpenAPI, standardized API errors, and automated tests are organized explicitly. A separate reference application demonstrates the architecture in a working domain without turning that domain into part of the reusable foundation. The goal isn't to claim that every SaaS should have the same architecture. It's much simpler: Stop solving the same foundation problems from zero when you could spend that effort on what makes your product different. If you're building a SaaS with Java and Spring Boot, you can explore the architecture, reference application, demo, and Founder License at: Top comments (0)
Comments
No comments yet. Start the discussion.