<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged spring-boot</title>
<link>https://coderlegion.com/tag/spring-boot</link>
<description>Powered by Question2Answer</description>
<item>
<title>The Legacy Report Was Going Away. The Agent Still Needed the Answer.</title>
<link>https://coderlegion.com/32464/the-legacy-report-was-going-away-the-agent-still-needed-the-answer</link>
<description>&lt;p&gt;&lt;em&gt;An anonymized banking modernization case study about replacing SAS-backed operational reports with APIs that agents could use in Salesforce.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Imagine an agent opening a fraud or exception report during a customer call. The report appears in a legacy system, but the agent’s real task is to make a timely, authorized decision. If the system is retired and the same answer becomes slow, incomplete, or visible to the wrong person, the migration has failed—even if the new application looks better.&lt;/p&gt;
&lt;p&gt;That was the challenge in a banking modernization program I worked on. Operational and business reporting functionality hosted on SAS needed to move into a Salesforce-centered workflow. My responsibilities included designing Spring Boot GET APIs for several report domains, connecting the service to Snowflake, applying agent role-based access, and exposing the endpoints to the Salesforce team. The architecture below is deliberately simplified; it describes the engineering pattern without disclosing internal systems or customer data.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=9107409727456458812&quot; alt=&quot;Simplified architecture: agent in Salesforce requests a report through a role-aware Spring Boot API backed by Snowflake&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;first-migrate-the-decision-not-the-screen&quot;&gt;First, migrate the decision—not the screen&lt;/h3&gt;
&lt;p&gt;A legacy report often contains more than a table. It can encode filters, calculations, refresh timing, exception definitions, and the habits people have built around it. Copying its columns into a new UI does not prove that an agent receives the same answer.&lt;/p&gt;
&lt;p&gt;For each report, the useful questions were: &lt;strong&gt;Who uses it? Which decision does it support? What does each field mean? How fresh must it be? What should happen when the data is missing?&lt;/strong&gt; These questions turn a screen replacement into a contract that the new service can implement and test.&lt;/p&gt;
&lt;p&gt;An example is an exception report. A row count can match while the underlying definition of “open” changes, or a date filter shifts at midnight in a different time zone. The result looks plausible, but an agent may follow up on the wrong cases. A migration test therefore needs representative records and business-rule comparisons, not only a schema check.&lt;/p&gt;
&lt;h3 id=&quot;put-a-stable-api-between-salesforce-and-reporting-data&quot;&gt;Put a stable API between Salesforce and reporting data&lt;/h3&gt;
&lt;p&gt;We used RESTful GET endpoints to give the Salesforce experience a focused way to request report data. A service layer can validate inputs, apply business rules, authorize the caller, query Snowflake, and return a consistent response contract. It also gives the UI team a stable interface while the data or report logic evolves behind it.&lt;/p&gt;
&lt;p&gt;Salesforce documents a &lt;strong&gt;request-and-reply&lt;/strong&gt; integration pattern for user actions that call an external system and wait for the result. That pattern is useful for interactive reporting, but it makes latency and failure behavior part of the user experience. A timeout is not an empty report. The API should distinguish “no matching records” from “the source was unavailable,” use bounded queries and pagination where appropriate, and make error states clear enough for the UI to handle.&lt;/p&gt;
&lt;p&gt;This service boundary also limits how much reporting logic leaks into the frontend. The API should expose business concepts the agent understands rather than Snowflake table names or a direct translation of old SAS implementation details.&lt;/p&gt;
&lt;h3 id=&quot;access-control-must-follow-the-data-all-the-way-through&quot;&gt;Access control must follow the data all the way through&lt;/h3&gt;
&lt;p&gt;“The agent is signed into Salesforce” is only the start of an authorization decision. The backend still needs to know which report and which records that agent may access. Role checks at the API boundary protect the endpoint; data-scoped controls protect the result set. The two should agree, and denied access should be tested as carefully as successful access.&lt;/p&gt;
&lt;p&gt;Snowflake supports row access policies that determine which rows a query returns. They are one possible defense in depth, subject to the organization’s roles and policy design. They also affect query behavior and performance, so access rules should be validated with realistic report requests. The point is broader than a particular Snowflake feature: &lt;strong&gt;a correct report returned to the wrong role is a production defect.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For a banking workflow, logs and traces also need restraint. A useful audit trail records who requested which report, when, and whether it succeeded. It does not need to dump sensitive report contents into application logs.&lt;/p&gt;
&lt;h3 id=&quot;prove-the-new-path-under-production-like-conditions&quot;&gt;Prove the new path under production-like conditions&lt;/h3&gt;
&lt;p&gt;Functional tests answer whether the endpoint returns the right data for a known case. They do not answer whether a team of agents can use it at the same time on a busy morning. We developed performance tests with JMeter and Gatling to exercise the services against SLA requirements under load.&lt;/p&gt;
&lt;p&gt;A meaningful test model includes the report mix, concurrent users, query sizes, authorized and unauthorized requests, and the effect of a slow data source. Measure end-to-end response time from the agent-facing API, not just the time taken by one SQL query. Check the long tail as well as the average. If a report is large, test pagination and the cost of repeated requests. If a dependency fails, verify that the service fails clearly and recovers.&lt;/p&gt;
&lt;p&gt;I would also treat the cutover as a comparison exercise: sample the same business cases through the legacy and new paths, explain any differences, and have report owners validate them. Where old behavior was wrong, document the intended correction explicitly. Otherwise a “fixed” report can look like an unexplained regression to the people using it.&lt;/p&gt;
&lt;h3 id=&quot;what-this-kind-of-migration-teaches&quot;&gt;What this kind of migration teaches&lt;/h3&gt;
&lt;p&gt;The visible technology changes from SAS reports to Salesforce screens, Spring Boot APIs, and Snowflake queries. The hard part is preserving the &lt;strong&gt;meaning, access, and timing&lt;/strong&gt; of the answer that an agent needs.&lt;/p&gt;
&lt;p&gt;The reusable sequence is straightforward: inventory the decisions supported by each report; define the API contract and data semantics; enforce access at the service and data layers; compare old and new results; test peak traffic and failure modes; then cut over with clear ownership of discrepancies. That is how a platform retirement becomes a reliable workflow change instead of a prettier route to the wrong answer.&lt;/p&gt;
&lt;p&gt;What has been the hardest part of a legacy reporting migration in your experience: report logic, data quality, access control, or cutover?&lt;/p&gt;
&lt;h4 id=&quot;references&quot;&gt;References&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Salesforce, &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.salesforce.com/docs/atlas.en-us.integration_patterns_and_practices.meta/integ_pat_patterns_platform.htm&quot;&gt;Integration Patterns&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Snowflake, &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.snowflake.com/en/user-guide/security-row-intro&quot;&gt;Understanding row access policies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Apache JMeter, &lt;a rel=&quot;nofollow&quot; href=&quot;https://jmeter.apache.org/usermanual/index.html&quot;&gt;User's Manual&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Gatling, &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.gatling.io/&quot;&gt;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32464/the-legacy-report-was-going-away-the-agent-still-needed-the-answer</guid>
<pubDate>Mon, 26 Oct 2026 04:08:02 +0000</pubDate>
</item>
<item>
<title>Spring Boot Authentication Boilerplate: From JWT Basics to Production-Ready Auth</title>
<link>https://coderlegion.com/27856/spring-boot-authentication-boilerplate-from-jwt-basics-to-production-ready-auth</link>
<description>&lt;p&gt;Starting a new Spring Boot project usually feels great.&lt;/p&gt;
&lt;p&gt;You create the project, add a few dependencies, write your first controller...&lt;/p&gt;
&lt;p&gt;Then authentication enters the chat.&lt;/p&gt;
&lt;p&gt;Suddenly you're configuring:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Spring Security&lt;/li&gt;
&lt;li&gt;JWT access tokens&lt;/li&gt;
&lt;li&gt;refresh tokens&lt;/li&gt;
&lt;li&gt;roles and permissions&lt;/li&gt;
&lt;li&gt;password hashing&lt;/li&gt;
&lt;li&gt;exception handling&lt;/li&gt;
&lt;li&gt;database migrations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Three hours later, your original feature still has exactly &lt;strong&gt;zero lines of code&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Congratulations. You built authentication again.&lt;/p&gt;
&lt;h3 id=&quot;the-auth-code-isn-t-the-interesting-part&quot;&gt;The auth code isn't the interesting part&lt;/h3&gt;
&lt;p&gt;Most applications need roughly the same authentication foundation.&lt;/p&gt;
&lt;p&gt;Register a user.&lt;/p&gt;
&lt;p&gt;Login.&lt;/p&gt;
&lt;p&gt;Return an access token.&lt;/p&gt;
&lt;p&gt;Refresh it safely.&lt;/p&gt;
&lt;p&gt;Protect endpoints.&lt;/p&gt;
&lt;p&gt;Check roles.&lt;/p&gt;
&lt;p&gt;Logout.&lt;/p&gt;
&lt;p&gt;Maybe add passkeys, OAuth, account recovery, or multiple sessions later.&lt;/p&gt;
&lt;p&gt;None of this is particularly unique to your SaaS, internal tool, API, or side project.&lt;/p&gt;
&lt;p&gt;But we keep rebuilding it.&lt;/p&gt;
&lt;p&gt;I was doing the same thing with my Spring Boot projects, so I eventually turned that repeated setup into &lt;strong&gt;AuthKit&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id=&quot;authkit-lite&quot;&gt;AuthKit Lite&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;AuthKit Lite&lt;/strong&gt; is the free and open-source version.&lt;/p&gt;
&lt;p&gt;It gives you a working Spring Boot authentication foundation with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JWT access tokens&lt;/li&gt;
&lt;li&gt;rotating refresh tokens&lt;/li&gt;
&lt;li&gt;Spring Security&lt;/li&gt;
&lt;li&gt;role-based authorization&lt;/li&gt;
&lt;li&gt;BCrypt passwords&lt;/li&gt;
&lt;li&gt;optional WebAuthn passkeys&lt;/li&gt;
&lt;li&gt;Flyway migrations&lt;/li&gt;
&lt;li&gt;H2 for instant local testing&lt;/li&gt;
&lt;li&gt;MySQL support&lt;/li&gt;
&lt;li&gt;integration tests&lt;/li&gt;
&lt;li&gt;a browser-based API testing page&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The goal is simple:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Clone → run → test the APIs → start building your application.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/buildbasekit/AuthKit-Lite.git
cd AuthKit-Lite
./mvnw spring-boot:run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then open:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;http://localhost:8080/api-test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No Docker ceremony.&lt;/p&gt;
&lt;p&gt;No spending your evening creating JWT filters for the 17th time.&lt;/p&gt;
&lt;p&gt;No pretending you'll &quot;clean up the auth code later.&quot;&lt;/p&gt;
&lt;p&gt;You can find AuthKit Lite here:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/authkit-lite/&quot;&gt;https://buildbasekit.com/boilerplates/authkit-lite/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;when-lite-isn-t-enough&quot;&gt;When Lite isn't enough&lt;/h3&gt;
&lt;p&gt;Basic authentication is enough for many projects.&lt;/p&gt;
&lt;p&gt;But production applications often grow into things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;email verification&lt;/li&gt;
&lt;li&gt;password recovery&lt;/li&gt;
&lt;li&gt;multiple user sessions&lt;/li&gt;
&lt;li&gt;session revocation&lt;/li&gt;
&lt;li&gt;refresh-token replay protection&lt;/li&gt;
&lt;li&gt;OAuth2 / OIDC&lt;/li&gt;
&lt;li&gt;account linking&lt;/li&gt;
&lt;li&gt;magic links&lt;/li&gt;
&lt;li&gt;passkeys&lt;/li&gt;
&lt;li&gt;account lifecycle handling&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is where &lt;strong&gt;AuthKit Pro&lt;/strong&gt; comes in.&lt;/p&gt;
&lt;p&gt;It takes the same idea further: provide the authentication foundation while keeping the application-specific business logic in your application.&lt;/p&gt;
&lt;p&gt;The important part for me was avoiding a giant framework that tries to own the entire application.&lt;/p&gt;
&lt;p&gt;Auth should be infrastructure.&lt;/p&gt;
&lt;p&gt;Not your new hobby.&lt;/p&gt;
&lt;p&gt;AuthKit Pro is here:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/authkit-pro&quot;&gt;https://buildbasekit.com/boilerplates/authkit-pro&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;lite-or-pro-&quot;&gt;Lite or Pro?&lt;/h3&gt;
&lt;p&gt;The easiest way to think about it:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AuthKit Lite&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Use it when you need a clean Spring Boot authentication starting point or want to understand and extend the implementation yourself.&lt;/p&gt;
&lt;p&gt;It's free and open source.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AuthKit Pro&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Use it when the project needs more complete authentication flows and production-focused session and account lifecycle features.&lt;/p&gt;
&lt;p&gt;Both are source-code foundations.&lt;/p&gt;
&lt;p&gt;You still own the application.&lt;/p&gt;
&lt;p&gt;You can change the architecture.&lt;/p&gt;
&lt;p&gt;You can remove features.&lt;/p&gt;
&lt;p&gt;You can add your own authorization rules.&lt;/p&gt;
&lt;p&gt;And your business logic doesn't have to live inside somebody else's authentication service.&lt;/p&gt;
&lt;h3 id=&quot;why-a-boilerplate-&quot;&gt;Why a boilerplate?&lt;/h3&gt;
&lt;p&gt;I'm not trying to eliminate authentication decisions.&lt;/p&gt;
&lt;p&gt;You still need to decide things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;token lifetimes&lt;/li&gt;
&lt;li&gt;authorization rules&lt;/li&gt;
&lt;li&gt;production secrets&lt;/li&gt;
&lt;li&gt;rate limiting&lt;/li&gt;
&lt;li&gt;monitoring&lt;/li&gt;
&lt;li&gt;application-specific permissions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A boilerplate just means you don't have to start those decisions from an empty &lt;code&gt;SecurityConfig.java&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That's the part I wanted to solve.&lt;/p&gt;
&lt;p&gt;Because there are much more interesting bugs waiting for us in the actual product.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;AuthKit Lite — Free &amp;amp; Open Source&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/authkit-lite/&quot;&gt;https://buildbasekit.com/boilerplates/authkit-lite/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AuthKit Pro — Production-Focused&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/authkit-pro&quot;&gt;https://buildbasekit.com/boilerplates/authkit-pro&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Built with Spring Boot and Spring Security.&lt;/p&gt;
&lt;p&gt;If you're building Spring Boot APIs, I'd also be interested to know:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Do you rebuild authentication for every project, maintain your own starter, or use an external identity provider?&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27856/spring-boot-authentication-boilerplate-from-jwt-basics-to-production-ready-auth</guid>
<pubDate>Sun, 20 Sep 2026 16:50:00 +0000</pubDate>
</item>
<item>
<title>AI Can Write Your Backend—But It Shouldn’t Rebuild the Foundation Every Time</title>
<link>https://coderlegion.com/20442/ai-can-write-your-backend-but-it-shouldnt-rebuild-the-foundation-every-time</link>
<description>&lt;p&gt;AI can generate a Spring Boot controller in seconds. It can scaffold entities, repositories, services, security filters, validation, and tests before you finish describing the feature.&lt;/p&gt;
&lt;p&gt;That speed looks transformative—until every new project starts with the same prompts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add JWT authentication&lt;/li&gt;
&lt;li&gt;Add refresh tokens&lt;/li&gt;
&lt;li&gt;Add role-based access control&lt;/li&gt;
&lt;li&gt;Configure Spring Security&lt;/li&gt;
&lt;li&gt;Add global exception handling&lt;/li&gt;
&lt;li&gt;Add request validation&lt;/li&gt;
&lt;li&gt;Add file uploads&lt;/li&gt;
&lt;li&gt;Add storage integration&lt;/li&gt;
&lt;li&gt;Fix the generated architecture&lt;/li&gt;
&lt;li&gt;Fix the tests after the architecture changes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At that point, AI is not accelerating product development. It is helping you repeatedly rebuild infrastructure your team has already solved.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;The more effective workflow is to start with a stable backend foundation and use AI to build the product-specific layer on top of it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That gives the coding agent real context, keeps architectural decisions consistent, reduces prompt size, and limits the amount of generated code your team must review.&lt;/p&gt;
&lt;h3 id=&quot;a-the-wrong-way-to-measure-ai-development-speed&quot;&gt;A. The Wrong Way to Measure AI Development Speed&lt;/h3&gt;
&lt;p&gt;A common mistake is measuring AI productivity by how quickly code appears.&lt;/p&gt;
&lt;p&gt;A prompt produces 20 files in two minutes, so the workflow feels fast. But generated output is only the first stage of delivery. The code still needs to be understood, reviewed, integrated, secured, tested, and maintained.&lt;/p&gt;
&lt;p&gt;For backend systems, the review cost can be substantial because the generated files are interconnected.&lt;/p&gt;
&lt;p&gt;Authentication affects authorization. Authorization affects endpoints. Endpoints depend on DTOs, services, repositories, validation, and exception handling. File operations may depend on authentication, ownership, metadata, and storage-provider configuration.&lt;/p&gt;
&lt;p&gt;A single inconsistency can break the entire flow.&lt;/p&gt;
&lt;p&gt;The better metric is not &lt;strong&gt;time to generated code&lt;/strong&gt;. It is &lt;strong&gt;time to a verified product feature&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;That distinction changes how AI should be used.&lt;/p&gt;
&lt;h3 id=&quot;b-why-rebuilding-backend-infrastructure-with-ai-creates-drift&quot;&gt;B. Why Rebuilding Backend Infrastructure With AI Creates Drift&lt;/h3&gt;
&lt;p&gt;Most backend applications need a familiar set of capabilities before product development begins:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;user registration and login&lt;/li&gt;
&lt;li&gt;password hashing&lt;/li&gt;
&lt;li&gt;access and refresh tokens&lt;/li&gt;
&lt;li&gt;protected endpoints&lt;/li&gt;
&lt;li&gt;roles and permissions&lt;/li&gt;
&lt;li&gt;request validation&lt;/li&gt;
&lt;li&gt;consistent API errors&lt;/li&gt;
&lt;li&gt;file upload and download APIs&lt;/li&gt;
&lt;li&gt;storage abstractions&lt;/li&gt;
&lt;li&gt;automated tests&lt;/li&gt;
&lt;li&gt;environment and security configuration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These capabilities matter, but they are rarely the reason a customer buys the product.&lt;/p&gt;
&lt;p&gt;Your customers care about the workflow your application enables. They care about the data it organizes, the task it automates, the integration it provides, or the operational problem it removes.&lt;/p&gt;
&lt;p&gt;They do not care that your team generated another custom JWT filter.&lt;/p&gt;
&lt;h4 id=&quot;1-separate-prompts-create-separate-architectural-decisions&quot;&gt;1. Separate prompts create separate architectural decisions&lt;/h4&gt;
&lt;p&gt;When authentication, uploads, validation, and error handling are generated through different prompts, the implementation can gradually lose consistency.&lt;/p&gt;
&lt;p&gt;Typical symptoms include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;controllers using different response formats&lt;/li&gt;
&lt;li&gt;services following different naming conventions&lt;/li&gt;
&lt;li&gt;repositories being accessed directly from unrelated layers&lt;/li&gt;
&lt;li&gt;duplicate DTOs representing the same data&lt;/li&gt;
&lt;li&gt;security rules that do not match newly generated routes&lt;/li&gt;
&lt;li&gt;token handling duplicated across filters and services&lt;/li&gt;
&lt;li&gt;file ownership checks implemented differently across endpoints&lt;/li&gt;
&lt;li&gt;tests that require different setup patterns for each feature&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each generated change may look reasonable in isolation. The system becomes difficult to maintain because the decisions do not form one coherent architecture.&lt;/p&gt;
&lt;h4 id=&quot;2-every-correction-expands-the-prompt&quot;&gt;2. Every correction expands the prompt&lt;/h4&gt;
&lt;p&gt;Once the code drifts, future prompts become larger:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Use the existing response wrapper, except for the upload controller. Keep the current token service, but do not copy the old refresh-token logic. Follow the package structure used by the billing module, not the user module. Do not modify the security filter because another feature depends on it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is not an efficient agent workflow. It is architectural debt being converted into prompt context.&lt;/p&gt;
&lt;h4 id=&quot;3-security-code-has-a-high-verification-cost&quot;&gt;3. Security code has a high verification cost&lt;/h4&gt;
&lt;p&gt;Generated security code can be syntactically valid and still be unsafe.&lt;/p&gt;
&lt;p&gt;A Spring Security configuration may accidentally expose a route. A refresh-token flow may fail to revoke old sessions. A file endpoint may authenticate users without checking file ownership. A validation rule may exist at the controller boundary but be bypassed internally.&lt;/p&gt;
&lt;p&gt;AI can help implement security-sensitive code, but it does not remove the need for human review and negative-path testing.&lt;/p&gt;
&lt;p&gt;The strategic move is to reduce how often that critical infrastructure must be regenerated.&lt;/p&gt;
&lt;h3 id=&quot;c-what-the-backend-foundation-should-already-provide&quot;&gt;C. What the Backend Foundation Should Already Provide&lt;/h3&gt;
&lt;p&gt;A reusable backend foundation should solve repeated infrastructure while leaving product behavior open for extension.&lt;/p&gt;
&lt;p&gt;It should not be a collection of copied snippets. It should be a working, documented application with clear boundaries.&lt;/p&gt;
&lt;h4 id=&quot;1-authentication-and-token-lifecycle&quot;&gt;1. Authentication and token lifecycle&lt;/h4&gt;
&lt;p&gt;The project should already define how registration, login, passwords, access tokens, refresh tokens, logout, expiration, and authentication failures work.&lt;/p&gt;
&lt;p&gt;The important part is not merely having JWT support. The entire lifecycle must be consistent.&lt;/p&gt;
&lt;h4 id=&quot;2-authorization&quot;&gt;2. Authorization&lt;/h4&gt;
&lt;p&gt;Roles and permissions should be enforced through a predictable mechanism, such as route-level or method-level authorization.&lt;/p&gt;
&lt;p&gt;Product code should not be filled with scattered checks such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;if (user.getRole().equals(&quot;ADMIN&quot;)) {
    // perform action
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The authorization model should be explicit enough that both developers and coding agents know how to protect a new feature.&lt;/p&gt;
&lt;h4 id=&quot;3-validation-and-exception-handling&quot;&gt;3. Validation and exception handling&lt;/h4&gt;
&lt;p&gt;Request validation should follow one standard, and API errors should have one recognizable structure.&lt;/p&gt;
&lt;p&gt;For example, a client should not receive three unrelated error shapes depending on whether the failure came from a controller, service, or security filter.&lt;/p&gt;
&lt;p&gt;Consistency improves frontend integration, automated testing, logging, and AI-generated extensions.&lt;/p&gt;
&lt;h4 id=&quot;4-file-handling-and-storage-boundaries&quot;&gt;4. File handling and storage boundaries&lt;/h4&gt;
&lt;p&gt;File systems become complicated quickly. A production-oriented design must consider:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ownership&lt;/li&gt;
&lt;li&gt;access control&lt;/li&gt;
&lt;li&gt;allowed file types&lt;/li&gt;
&lt;li&gt;size limits&lt;/li&gt;
&lt;li&gt;naming&lt;/li&gt;
&lt;li&gt;metadata&lt;/li&gt;
&lt;li&gt;streaming&lt;/li&gt;
&lt;li&gt;deletion&lt;/li&gt;
&lt;li&gt;retention&lt;/li&gt;
&lt;li&gt;local or cloud storage&lt;/li&gt;
&lt;li&gt;public versus private delivery&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Product services should not depend directly on S3, local disk paths, or another storage vendor. They should depend on a storage boundary that can be replaced or extended.&lt;/p&gt;
&lt;h4 id=&quot;5-clear-application-layers&quot;&gt;5. Clear application layers&lt;/h4&gt;
&lt;p&gt;The application should communicate where each responsibility belongs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;src/main/java/com/example/app/
├── controller/
├── service/
├── security/
├── model/
├── repository/
├── dto/
├── exception/
└── config/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The exact folder names are not the key issue. Predictable ownership is.&lt;/p&gt;
&lt;p&gt;Controllers should handle HTTP concerns. Services should own business workflows. Repositories should handle persistence. Security should establish identity and access. Storage integrations should remain behind a focused abstraction.&lt;/p&gt;
&lt;h4 id=&quot;6-a-usable-testing-strategy&quot;&gt;6. A usable testing strategy&lt;/h4&gt;
&lt;p&gt;The project should make it practical to test new features without rebuilding unrelated infrastructure for every test.&lt;/p&gt;
&lt;p&gt;That means having examples for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;service tests&lt;/li&gt;
&lt;li&gt;authenticated endpoint tests&lt;/li&gt;
&lt;li&gt;role-based authorization tests&lt;/li&gt;
&lt;li&gt;validation failures&lt;/li&gt;
&lt;li&gt;repository integration tests&lt;/li&gt;
&lt;li&gt;file-access and ownership tests&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;An AI agent produces better tests when it can follow an existing test pattern.&lt;/p&gt;
&lt;h3 id=&quot;d-a-better-ai-assisted-backend-workflow&quot;&gt;D. A Better AI-Assisted Backend Workflow&lt;/h3&gt;
&lt;p&gt;The following workflow shifts AI from infrastructure generator to product implementation partner.&lt;/p&gt;
&lt;h4 id=&quot;1-begin-with-a-known-backend-foundation&quot;&gt;1. Begin with a known backend foundation&lt;/h4&gt;
&lt;p&gt;For product delivery, an empty repository is usually the least efficient starting point.&lt;/p&gt;
&lt;p&gt;Use a backend foundation that already implements and documents the repeated concerns. BuildBaseKit currently provides several Spring Boot starting points:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/authkit-lite/&quot;&gt;AuthKit-Lite&lt;/a&gt; for JWT authentication, refresh tokens, and role-based access&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/filora-fs-lite/&quot;&gt;FiloraFS-Lite&lt;/a&gt; for lightweight local file-storage APIs&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/filora-fs-pro/&quot;&gt;FiloraFS-Pro&lt;/a&gt; for authentication, S3 integration, file management, and streaming workflows&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The objective is not to avoid understanding backend infrastructure. The objective is to avoid paying the implementation and verification cost every time a new product starts.&lt;/p&gt;
&lt;h4 id=&quot;2-verify-the-baseline-before-extending-it&quot;&gt;2. Verify the baseline before extending it&lt;/h4&gt;
&lt;p&gt;Before asking an AI agent to make changes, run the project and verify the existing flows.&lt;/p&gt;
&lt;p&gt;Confirm that these behaviors work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;registration&lt;/li&gt;
&lt;li&gt;login&lt;/li&gt;
&lt;li&gt;access-token authentication&lt;/li&gt;
&lt;li&gt;refresh-token rotation or renewal&lt;/li&gt;
&lt;li&gt;protected routes&lt;/li&gt;
&lt;li&gt;role restrictions&lt;/li&gt;
&lt;li&gt;validation errors&lt;/li&gt;
&lt;li&gt;global exception responses&lt;/li&gt;
&lt;li&gt;file upload and retrieval&lt;/li&gt;
&lt;li&gt;existing automated tests&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This gives the team a known-good baseline.&lt;/p&gt;
&lt;p&gt;Without a baseline, a later failure becomes harder to diagnose. The issue may come from the original foundation, the generated feature, or the interaction between both.&lt;/p&gt;
&lt;h4 id=&quot;3-put-architectural-context-inside-the-repository&quot;&gt;3. Put architectural context inside the repository&lt;/h4&gt;
&lt;p&gt;An AI agent should not have to reverse-engineer the project rules from scratch during every task.&lt;/p&gt;
&lt;p&gt;Useful repository documents include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AGENTS.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ARCHITECTURE.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AI_RULES.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CONTRIBUTING.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AGENT_CONTRIBUTING.md&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These files should answer practical questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Which layer owns business logic?&lt;/li&gt;
&lt;li&gt;How are DTOs named?&lt;/li&gt;
&lt;li&gt;What API response format is required?&lt;/li&gt;
&lt;li&gt;Which authentication flows must remain unchanged?&lt;/li&gt;
&lt;li&gt;How should new permissions be implemented?&lt;/li&gt;
&lt;li&gt;Which commands run unit and integration tests?&lt;/li&gt;
&lt;li&gt;What files should not be modified?&lt;/li&gt;
&lt;li&gt;What must pass before a task is complete?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;BuildBaseKit's &lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/features/ai-ready/&quot;&gt;AI-ready project foundations&lt;/a&gt; use repository-level instructions to give coding agents this context before they edit the application.&lt;/p&gt;
&lt;p&gt;The repository then becomes more than source code. It becomes the operating model for both human and AI contributors.&lt;/p&gt;
&lt;h4 id=&quot;4-request-one-bounded-product-change&quot;&gt;4. Request one bounded product change&lt;/h4&gt;
&lt;p&gt;Broad prompts create broad patches.&lt;/p&gt;
&lt;p&gt;A prompt such as this gives the model too much control:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Build a complete secure Spring Boot backend with JWT authentication, refresh tokens, roles, validation, exception handling, file uploads, S3 storage, and tests.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A bounded prompt is much more effective:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Add an organization invitation endpoint using the existing authentication, validation, exception, and email patterns. Only organization admins can invite members. Add service and endpoint tests. Do not modify the token flow.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Other strong examples include:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Add admin-only file approval using the current role annotations and file metadata service.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;Implement subscription cancellation inside the existing billing module. Preserve the current controller response format.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;Add an audit event when a user deletes a file. Follow the existing event-publishing pattern and include tests.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;These requests are easier to review because the desired behavior and the protected boundaries are explicit.&lt;/p&gt;
&lt;h4 id=&quot;5-review-the-patch-as-a-change-not-as-a-new-system&quot;&gt;5. Review the patch as a change, not as a new system&lt;/h4&gt;
&lt;p&gt;A stable foundation allows the generated output to remain small.&lt;/p&gt;
&lt;p&gt;Review can focus on questions such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Does the new code follow the existing architecture?&lt;/li&gt;
&lt;li&gt;Is authorization enforced at the correct boundary?&lt;/li&gt;
&lt;li&gt;Are ownership and tenant checks present?&lt;/li&gt;
&lt;li&gt;Does the feature reuse existing validation and error handling?&lt;/li&gt;
&lt;li&gt;Are database operations safe and transactional where needed?&lt;/li&gt;
&lt;li&gt;Do tests cover unauthorized and invalid requests?&lt;/li&gt;
&lt;li&gt;Did the agent modify unrelated infrastructure?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is significantly more efficient than reviewing a newly invented security and storage architecture for every feature.&lt;/p&gt;
&lt;h4 id=&quot;6-test-the-business-behavior&quot;&gt;6. Test the business behavior&lt;/h4&gt;
&lt;p&gt;Suppose the new feature is organization invitations.&lt;/p&gt;
&lt;p&gt;The useful test cases are now focused:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;an authorized admin can create an invitation&lt;/li&gt;
&lt;li&gt;a regular member cannot create an invitation&lt;/li&gt;
&lt;li&gt;an invitation belongs to the correct organization&lt;/li&gt;
&lt;li&gt;duplicate invitations are handled correctly&lt;/li&gt;
&lt;li&gt;expired invitations cannot be accepted&lt;/li&gt;
&lt;li&gt;invalid email addresses return the standard validation response&lt;/li&gt;
&lt;li&gt;unauthenticated requests are rejected&lt;/li&gt;
&lt;li&gt;the response follows the existing API contract&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The team is testing the feature instead of simultaneously debugging JWT parsing, route configuration, persistence setup, and business logic.&lt;/p&gt;
&lt;h3 id=&quot;e-why-this-workflow-uses-fewer-tokens&quot;&gt;E. Why This Workflow Uses Fewer Tokens&lt;/h3&gt;
&lt;p&gt;A large prompt often repeats information the repository should already contain:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;technology stack&lt;/li&gt;
&lt;li&gt;package structure&lt;/li&gt;
&lt;li&gt;authentication requirements&lt;/li&gt;
&lt;li&gt;token behavior&lt;/li&gt;
&lt;li&gt;role definitions&lt;/li&gt;
&lt;li&gt;validation conventions&lt;/li&gt;
&lt;li&gt;error response shape&lt;/li&gt;
&lt;li&gt;file-storage rules&lt;/li&gt;
&lt;li&gt;testing expectations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When those decisions already exist in code and documentation, the prompt can describe only the change.&lt;/p&gt;
&lt;p&gt;Compare these two requests.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Infrastructure-first prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Generate JWT authentication for Spring Boot with refresh tokens, role-based authorization, validation, exception handling, and secure file uploads.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Foundation-first prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Add a team invitation endpoint using the existing authentication and validation patterns.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The second request is smaller, but token reduction is not the only advantage.&lt;/p&gt;
&lt;p&gt;It should also result in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;fewer generated files&lt;/li&gt;
&lt;li&gt;fewer architectural decisions&lt;/li&gt;
&lt;li&gt;a smaller review surface&lt;/li&gt;
&lt;li&gt;fewer opportunities for unrelated regressions&lt;/li&gt;
&lt;li&gt;more focused tests&lt;/li&gt;
&lt;li&gt;easier rollback&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The highest-value optimization is reducing the size and uncertainty of the change.&lt;/p&gt;
&lt;h3 id=&quot;f-why-existing-context-improves-generated-code&quot;&gt;F. Why Existing Context Improves Generated Code&lt;/h3&gt;
&lt;p&gt;A blank repository gives the agent maximum freedom. That is often presented as an advantage, but for production systems it creates unnecessary risk.&lt;/p&gt;
&lt;p&gt;The model must invent:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the package structure&lt;/li&gt;
&lt;li&gt;naming rules&lt;/li&gt;
&lt;li&gt;service boundaries&lt;/li&gt;
&lt;li&gt;DTO patterns&lt;/li&gt;
&lt;li&gt;API responses&lt;/li&gt;
&lt;li&gt;exception types&lt;/li&gt;
&lt;li&gt;token handling&lt;/li&gt;
&lt;li&gt;permission enforcement&lt;/li&gt;
&lt;li&gt;persistence conventions&lt;/li&gt;
&lt;li&gt;test setup&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;An existing project turns those open decisions into constraints.&lt;/p&gt;
&lt;p&gt;Constraints make the output more predictable.&lt;/p&gt;
&lt;p&gt;An agent can inspect a current feature, identify the controller–service–repository pattern, and implement the next feature in the same way. It can copy the structure without copying the business logic.&lt;/p&gt;
&lt;p&gt;That is the core principle behind AI-ready architecture:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Do not ask the agent to make every decision. Give it a system in which the important decisions have already been made.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;g-common-failure-modes&quot;&gt;G. Common Failure Modes&lt;/h3&gt;
&lt;h4 id=&quot;1-regenerating-authentication-for-each-application&quot;&gt;1. Regenerating authentication for each application&lt;/h4&gt;
&lt;p&gt;Authentication is repetitive and security-sensitive. Reuse a reviewed implementation and customize only the behavior the product actually requires.&lt;/p&gt;
&lt;h4 id=&quot;2-adding-uploads-without-defining-access-rules&quot;&gt;2. Adding uploads without defining access rules&lt;/h4&gt;
&lt;p&gt;A file upload endpoint is not complete when a file reaches disk or S3.&lt;/p&gt;
&lt;p&gt;Before implementation, define:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;who owns the file&lt;/li&gt;
&lt;li&gt;who may read it&lt;/li&gt;
&lt;li&gt;who may delete it&lt;/li&gt;
&lt;li&gt;allowed formats&lt;/li&gt;
&lt;li&gt;maximum size&lt;/li&gt;
&lt;li&gt;whether it is public or private&lt;/li&gt;
&lt;li&gt;how metadata is stored&lt;/li&gt;
&lt;li&gt;how downloads are authorized&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;3-mixing-business-rules-with-spring-security&quot;&gt;3. Mixing business rules with Spring Security&lt;/h4&gt;
&lt;p&gt;Security should identify the caller and enforce access boundaries. Product services should implement the workflow.&lt;/p&gt;
&lt;p&gt;When business behavior is buried inside filters or security configuration, both the code and the generated extensions become harder to reason about.&lt;/p&gt;
&lt;h4 id=&quot;4-trusting-plausible-generated-security-code&quot;&gt;4. Trusting plausible generated security code&lt;/h4&gt;
&lt;p&gt;Security code often looks convincing. That is not evidence that it is correct.&lt;/p&gt;
&lt;p&gt;Review route coverage, permission checks, token expiration, refresh behavior, logout semantics, CORS, CSRF assumptions, and error handling. Test unauthorized and cross-tenant access explicitly.&lt;/p&gt;
&lt;h4 id=&quot;5-sending-one-massive-implementation-prompt&quot;&gt;5. Sending one massive implementation prompt&lt;/h4&gt;
&lt;p&gt;A large prompt encourages the agent to create a large, tightly coupled patch.&lt;/p&gt;
&lt;p&gt;Split the work into independently verifiable units. A smaller patch is easier to understand, test, revert, and maintain.&lt;/p&gt;
&lt;h4 id=&quot;6-measuring-generated-lines-instead-of-delivered-value&quot;&gt;6. Measuring generated lines instead of delivered value&lt;/h4&gt;
&lt;p&gt;More generated code can mean more review work.&lt;/p&gt;
&lt;p&gt;Measure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;time to a working feature&lt;/li&gt;
&lt;li&gt;defect rate&lt;/li&gt;
&lt;li&gt;regression rate&lt;/li&gt;
&lt;li&gt;review time&lt;/li&gt;
&lt;li&gt;test coverage&lt;/li&gt;
&lt;li&gt;architectural consistency&lt;/li&gt;
&lt;li&gt;security findings&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those metrics reflect delivery quality. Lines of code do not.&lt;/p&gt;
&lt;h3 id=&quot;h-when-building-from-scratch-is-still-the-right-choice&quot;&gt;H. When Building From Scratch Is Still the Right Choice&lt;/h3&gt;
&lt;p&gt;A reusable foundation is not always the correct answer.&lt;/p&gt;
&lt;p&gt;Building the infrastructure yourself makes sense when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;your primary goal is learning Spring Security or storage internals&lt;/li&gt;
&lt;li&gt;the application has unusual authentication requirements&lt;/li&gt;
&lt;li&gt;the project is governed by specialized compliance controls&lt;/li&gt;
&lt;li&gt;the architecture differs fundamentally from the available foundation&lt;/li&gt;
&lt;li&gt;adapting the boilerplate would take more effort than creating the system directly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A useful engineering rule is:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Build it from scratch when the infrastructure itself is part of the learning or differentiation. Reuse it when it is only a prerequisite for the real product.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Developers should understand the systems they reuse. Reuse is not a substitute for knowledge. It is a way to apply that knowledge without repeating the same implementation work indefinitely.&lt;/p&gt;
&lt;p&gt;For a more detailed comparison, read &lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/blogs/spring-boot-authentication-boilerplate/&quot;&gt;Spring Boot Authentication Boilerplate vs. Building From Scratch&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;i-build-the-product-layer-not-the-setup-layer&quot;&gt;I. Build the Product Layer, Not the Setup Layer&lt;/h3&gt;
&lt;p&gt;AI coding tools provide the most leverage when they operate inside a stable system.&lt;/p&gt;
&lt;p&gt;The backend foundation should provide the repeated infrastructure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;authentication&lt;/li&gt;
&lt;li&gt;authorization&lt;/li&gt;
&lt;li&gt;validation&lt;/li&gt;
&lt;li&gt;exception handling&lt;/li&gt;
&lt;li&gt;storage boundaries&lt;/li&gt;
&lt;li&gt;application structure&lt;/li&gt;
&lt;li&gt;test patterns&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The coding agent should focus on what makes the product different:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;domain workflows&lt;/li&gt;
&lt;li&gt;integrations&lt;/li&gt;
&lt;li&gt;automation&lt;/li&gt;
&lt;li&gt;permissions specific to the business&lt;/li&gt;
&lt;li&gt;reporting&lt;/li&gt;
&lt;li&gt;billing behavior&lt;/li&gt;
&lt;li&gt;collaboration features&lt;/li&gt;
&lt;li&gt;operational rules&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The developer remains responsible for architecture, security, review, and delivery quality.&lt;/p&gt;
&lt;p&gt;This division of responsibility creates a more scalable workflow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The foundation supplies trusted infrastructure.&lt;/li&gt;
&lt;li&gt;Repository documentation supplies context.&lt;/li&gt;
&lt;li&gt;AI implements a bounded product change.&lt;/li&gt;
&lt;li&gt;Tests verify the new behavior.&lt;/li&gt;
&lt;li&gt;Human review protects architecture and security.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That is a better operating model than asking an AI tool to rebuild the entire backend every time.&lt;/p&gt;
&lt;h3 id=&quot;j-start-with-a-stable-spring-boot-foundation&quot;&gt;J. Start With a Stable Spring Boot Foundation&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/&quot;&gt;BuildBaseKit&lt;/a&gt; provides Spring Boot foundations for authentication, file storage, and reusable backend architecture.&lt;/p&gt;
&lt;p&gt;The projects are designed to help developers and coding agents begin with working infrastructure, explicit conventions, and a predictable structure.&lt;/p&gt;
&lt;p&gt;Explore the &lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/&quot;&gt;available Spring Boot boilerplates&lt;/a&gt; or review the approach behind &lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/features/ai-ready/&quot;&gt;AI-ready project context&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;k-frequently-asked-questions&quot;&gt;K. Frequently Asked Questions&lt;/h3&gt;
&lt;h4 id=&quot;1-does-using-a-boilerplate-limit-product-flexibility-&quot;&gt;1. Does using a boilerplate limit product flexibility?&lt;/h4&gt;
&lt;p&gt;Not inherently. A good foundation standardizes repeated infrastructure while leaving domain models, workflows, integrations, and product services open for extension.&lt;/p&gt;
&lt;p&gt;The risk comes from using a rigid or poorly understood foundation, not from reuse itself.&lt;/p&gt;
&lt;h4 id=&quot;2-why-does-this-approach-improve-ai-output-&quot;&gt;2. Why does this approach improve AI output?&lt;/h4&gt;
&lt;p&gt;The coding agent has patterns to follow. It can inspect existing controllers, services, authorization rules, DTOs, exceptions, and tests instead of inventing each convention.&lt;/p&gt;
&lt;h4 id=&quot;3-does-a-foundation-automatically-make-generated-code-secure-&quot;&gt;3. Does a foundation automatically make generated code secure?&lt;/h4&gt;
&lt;p&gt;No.&lt;/p&gt;
&lt;p&gt;It reduces the number of security decisions being regenerated, but every new authorization rule, data-access path, file operation, and integration still requires review and testing.&lt;/p&gt;
&lt;h4 id=&quot;4-is-this-workflow-specific-to-spring-boot-&quot;&gt;4. Is this workflow specific to Spring Boot?&lt;/h4&gt;
&lt;p&gt;The principle applies to most backend stacks. It is especially useful in Spring Boot because authentication, Spring Security, validation, persistence, and application layering involve many connected configuration and design decisions.&lt;/p&gt;
&lt;h4 id=&quot;5-should-developers-still-learn-how-the-infrastructure-works-&quot;&gt;5. Should developers still learn how the infrastructure works?&lt;/h4&gt;
&lt;p&gt;Yes.&lt;/p&gt;
&lt;p&gt;A foundation should be readable and modifiable. The goal is to stop repeating solved setup work, not to turn backend infrastructure into a black box.&lt;/p&gt;
&lt;h3 id=&quot;l-related-buildbasekit-articles&quot;&gt;L. Related BuildBaseKit Articles&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/blogs/spring-boot-jwt-authentication-architecture/&quot;&gt;Spring Boot Authentication Architecture: JWT and Clean Setup&lt;/a&gt; — organize authentication without mixing token logic and product workflows.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/blogs/spring-boot-authentication-boilerplate/&quot;&gt;Spring Boot Authentication Boilerplate vs. Building From Scratch&lt;/a&gt; — evaluate the trade-offs between reuse, control, learning, and delivery speed.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/blogs/spring-boot-file-upload-production-guide/&quot;&gt;Spring Boot File Upload: Production-Ready System Design Guide&lt;/a&gt; — design secure uploads, metadata, storage, and delivery.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/blogs/spring-boot-file-storage-local-guide/&quot;&gt;How to Store and Serve Files in Spring Boot&lt;/a&gt; — build a local-storage implementation with clean boundaries.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/blogs/how-to-deploy-a-production-ready-file-server-on-a-vps-for-free/&quot;&gt;How to Deploy a Production-Ready File Server on a VPS&lt;/a&gt; — cover the deployment and operational requirements around a Spring Boot file server.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published by &lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/blogs/build-your-backend-with-ai/&quot;&gt;BuildBaseKit&lt;/a&gt;. This CoderLegion edition has been restructured and edited for the CoderLegion developer community.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20442/ai-can-write-your-backend-but-it-shouldnt-rebuild-the-foundation-every-time</guid>
<pubDate>Fri, 12 Jun 2026 17:16:39 +0000</pubDate>
</item>
<item>
<title>Upload Files to AWS S3 with Spring Boot (Clean Architecture Guide)</title>
<link>https://coderlegion.com/14577/upload-files-to-aws-s3-with-spring-boot-clean-architecture-guide</link>
<description>&lt;p&gt;Uploading files to AWS S3 in Spring Boot is simple.&lt;/p&gt;
&lt;p&gt;Designing it properly for production is where most developers struggle.&lt;/p&gt;
&lt;p&gt;Most implementations end up like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;controllers handling everything&lt;/li&gt;
&lt;li&gt;direct S3 calls everywhere&lt;/li&gt;
&lt;li&gt;no structure&lt;/li&gt;
&lt;li&gt;hard to scale later&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this guide, you’ll learn how to build a clean, scalable file upload system.&lt;/p&gt;
&lt;p&gt;Building file uploads in production is harder than it looks.&lt;/p&gt;
&lt;p&gt;If you want a clean, ready-to-use setup instead of building everything yourself:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/filora-fs-pro/&quot;&gt;https://buildbasekit.com/boilerplates/filora-fs-pro/&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;quick-answer&quot;&gt;Quick Answer&lt;/h3&gt;
&lt;p&gt;The best way to upload files to S3 in Spring Boot:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;separate storage logic into a service layer  &lt;/li&gt;
&lt;li&gt;avoid direct S3 calls in controllers  &lt;/li&gt;
&lt;li&gt;use pre-signed URLs for scalability  &lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-use-aws-s3&quot;&gt;Why use AWS S3&lt;/h3&gt;
&lt;p&gt;S3 is the standard for file storage in backend systems.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;handles large files and high traffic  &lt;/li&gt;
&lt;li&gt;highly scalable and durable  &lt;/li&gt;
&lt;li&gt;no infrastructure management  &lt;/li&gt;
&lt;li&gt;supports secure access control  &lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;when-to-use-s3-vs-local-storage&quot;&gt;When to use S3 vs local storage&lt;/h3&gt;
&lt;p&gt;Use S3 when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;building production systems  &lt;/li&gt;
&lt;li&gt;handling user uploads  &lt;/li&gt;
&lt;li&gt;running multiple instances  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Use local storage when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;prototyping  &lt;/li&gt;
&lt;li&gt;small internal tools  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Avoid local storage in distributed systems.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;high-level-architecture&quot;&gt;High-level architecture&lt;/h3&gt;
&lt;p&gt;A clean file upload system should look like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Controller → handles incoming requests  &lt;/li&gt;
&lt;li&gt;Service → processes file logic  &lt;/li&gt;
&lt;li&gt;Storage layer → interacts with S3  &lt;/li&gt;
&lt;li&gt;Database → stores file metadata  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This separation keeps your code maintainable.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;file-metadata-design&quot;&gt;File metadata design&lt;/h3&gt;
&lt;p&gt;Do not just upload files.&lt;/p&gt;
&lt;p&gt;Store metadata like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;file name  &lt;/li&gt;
&lt;li&gt;unique ID  &lt;/li&gt;
&lt;li&gt;S3 object key  &lt;/li&gt;
&lt;li&gt;file size and type  &lt;/li&gt;
&lt;li&gt;upload timestamp  &lt;/li&gt;
&lt;li&gt;user reference  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This enables search, access control, and tracking.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;example-api-design&quot;&gt;Example API design&lt;/h3&gt;
&lt;p&gt;A simple API structure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;POST /files → upload file  &lt;/li&gt;
&lt;li&gt;GET /files/{id} → fetch metadata  &lt;/li&gt;
&lt;li&gt;GET /files/{id}/url → get access URL  &lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;upload-flow&quot;&gt;Upload flow&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;client sends upload request  &lt;/li&gt;
&lt;li&gt;backend validates file  &lt;/li&gt;
&lt;li&gt;file is uploaded to S3  &lt;/li&gt;
&lt;li&gt;metadata is stored in database  &lt;/li&gt;
&lt;li&gt;response returns file reference  &lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;keep-storage-logic-abstract&quot;&gt;Keep storage logic abstract&lt;/h3&gt;
&lt;p&gt;Do not tightly couple your app with S3.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;define a storage interface  &lt;/li&gt;
&lt;li&gt;implement S3 separately  &lt;/li&gt;
&lt;li&gt;allow switching providers later  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This makes your system flexible.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;security-considerations&quot;&gt;Security considerations&lt;/h3&gt;
&lt;p&gt;Always handle security properly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;validate file type and size  &lt;/li&gt;
&lt;li&gt;avoid exposing S3 bucket directly  &lt;/li&gt;
&lt;li&gt;manage credentials securely  &lt;/li&gt;
&lt;li&gt;use pre-signed URLs  &lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;pre-signed-urls-important-&quot;&gt;Pre-signed URLs (important)&lt;/h3&gt;
&lt;p&gt;Better approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;backend generates upload URL  &lt;/li&gt;
&lt;li&gt;client uploads directly to S3  &lt;/li&gt;
&lt;li&gt;backend stores metadata  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;reduces server load  &lt;/li&gt;
&lt;li&gt;improves scalability  &lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;common-mistakes&quot;&gt;Common mistakes&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;calling S3 directly from controller  &lt;/li&gt;
&lt;li&gt;hardcoding credentials  &lt;/li&gt;
&lt;li&gt;skipping metadata storage  &lt;/li&gt;
&lt;li&gt;mixing upload and access logic  &lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final thoughts&lt;/h3&gt;
&lt;p&gt;Uploading files to S3 is easy.&lt;/p&gt;
&lt;p&gt;Designing a clean system is what matters long term.&lt;/p&gt;
&lt;p&gt;If you skip structure now, it will become painful later.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;skip-the-setup&quot;&gt;Skip the setup&lt;/h3&gt;
&lt;p&gt;If you're building anything serious, don't spend days structuring this.&lt;/p&gt;
&lt;p&gt;Use a production-ready backend instead:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/filora-fs-pro/&quot;&gt;https://buildbasekit.com/boilerplates/filora-fs-pro/&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;clean architecture  &lt;/li&gt;
&lt;li&gt;S3 + pre-signed URLs  &lt;/li&gt;
&lt;li&gt;ready APIs  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Build faster. Focus on real features.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14577/upload-files-to-aws-s3-with-spring-boot-clean-architecture-guide</guid>
<pubDate>Sun, 12 Apr 2026 16:42:50 +0000</pubDate>
</item>
<item>
<title>How to Deploy a Production-Ready File Server on a VPS for Free</title>
<link>https://coderlegion.com/14585/how-to-deploy-a-production-ready-file-server-on-a-vps-for-free</link>
<description>&lt;p&gt;Deploying a file server sounds simple.&lt;/p&gt;
&lt;p&gt;Making it production-ready is where most developers struggle.&lt;/p&gt;
&lt;p&gt;You don’t just need to run the app.&lt;br&gt;
You need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;keep it running&lt;/li&gt;
&lt;li&gt;secure it&lt;/li&gt;
&lt;li&gt;handle traffic properly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this guide, you’ll deploy a real file server using Spring Boot, Nginx, and a VPS.&lt;/p&gt;
&lt;h3 id=&quot;start-with-a-ready-backend&quot;&gt;Start with a ready backend&lt;/h3&gt;
&lt;p&gt;Instead of building everything from scratch, you can start with a ready file server backend.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/filora-fs-lite/&quot;&gt;https://buildbasekit.com/boilerplates/filora-fs-lite/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It already includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;file upload APIs&lt;/li&gt;
&lt;li&gt;clean project structure&lt;/li&gt;
&lt;li&gt;production-ready setup&lt;/li&gt;
&lt;li&gt;easy deployment flow (used in this guide)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can follow this tutorial directly using this boilerplate.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-you-ll-build&quot;&gt;What you’ll build&lt;/h3&gt;
&lt;p&gt;By the end, you’ll have a file server that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;accepts file uploads  &lt;/li&gt;
&lt;li&gt;runs continuously with systemd  &lt;/li&gt;
&lt;li&gt;serves traffic via Nginx  &lt;/li&gt;
&lt;li&gt;uses HTTPS (SSL)  &lt;/li&gt;
&lt;li&gt;is protected with a firewall  &lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;VPS (Ubuntu recommended)  &lt;/li&gt;
&lt;li&gt;Java installed  &lt;/li&gt;
&lt;li&gt;your Spring Boot jar ready  &lt;/li&gt;
&lt;li&gt;domain (optional but needed for HTTPS)  &lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-prepare-your-app&quot;&gt;1. Prepare your app&lt;/h3&gt;
&lt;p&gt;Do not hardcode configs.&lt;/p&gt;
&lt;p&gt;Use environment variables instead.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
spring.application.name=filorafs
server.port=8080
storage.local.path=/opt/filora/uploads

&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-connect-to-your-vps&quot;&gt;2. Connect to your VPS&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;
ssh -i your-key.key ubuntu@your-ip

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Install dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y
sudo apt install openjdk-21-jdk nginx ufw -y

&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-setup-app-directory&quot;&gt;3. Setup app directory&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;
sudo mkdir -p /opt/filora
sudo chown -R $USER:$USER /opt/filora

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Upload your jar:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
scp target/app.jar ubuntu@your-ip:/opt/filora/

&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-add-environment-variables&quot;&gt;4. Add environment variables&lt;/h3&gt;
&lt;p&gt;Create &lt;code&gt;.env&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
DB_PASSWORD=your_password
SERVER_PORT=8080
STORAGE_PATH=/opt/filora/uploads

&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-run-app-manually&quot;&gt;5. Run app manually&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;
cd /opt/filora
source .env
java -jar app.jar

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If it works → move forward.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-setup-systemd-important-&quot;&gt;6. Setup systemd (important)&lt;/h3&gt;
&lt;p&gt;Create service:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
sudo nano /etc/systemd/system/app.service

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Paste:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
[Unit]
Description=File Server
After=network.target

[Service]
User=ubuntu
WorkingDirectory=/opt/filora
EnvironmentFile=/opt/filora/.env
ExecStart=/usr/bin/java -jar /opt/filora/app.jar
Restart=always

[Install]
WantedBy=multi-user.target

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Start service:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
sudo systemctl daemon-reload
sudo systemctl enable app
sudo systemctl start app

&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;7-setup-nginx&quot;&gt;7. Setup Nginx&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;
sudo nano /etc/nginx/sites-available/app

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;
server {
listen 80;
server_name yourdomain.com;

client_max_body_size 100M;

location / {
    proxy_pass http://localhost:8080;
}

}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Enable:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
sudo ln -s /etc/nginx/sites-available/app /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;8-add-https&quot;&gt;8. Add HTTPS&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d yourdomain.com

&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;9-secure-server&quot;&gt;9. Secure server&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Do NOT expose port 8080 publicly.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;10-test-everything&quot;&gt;10. Test everything&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;upload file  &lt;/li&gt;
&lt;li&gt;check logs  &lt;/li&gt;
&lt;li&gt;verify HTTPS  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Logs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
journalctl -u app -f

&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;common-mistakes-1&quot;&gt;Common mistakes&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;exposing backend port directly  &lt;/li&gt;
&lt;li&gt;wrong file permissions  &lt;/li&gt;
&lt;li&gt;hardcoded configs  &lt;/li&gt;
&lt;li&gt;skipping HTTPS  &lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thoughts-1&quot;&gt;Final thoughts&lt;/h3&gt;
&lt;p&gt;A file server is easy to build.&lt;/p&gt;
&lt;p&gt;A production-ready setup is what actually matters.&lt;/p&gt;
&lt;p&gt;Once you understand this flow:&lt;br&gt;
build → deploy → proxy → secure&lt;/p&gt;
&lt;p&gt;you can deploy almost any backend.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;skip-the-setup-1&quot;&gt;Skip the setup&lt;/h3&gt;
&lt;p&gt;If you don’t want to do all this manually:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/filora-fs-lite/&quot;&gt;https://buildbasekit.com/boilerplates/filora-fs-lite/&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ready backend  &lt;/li&gt;
&lt;li&gt;file upload APIs  &lt;/li&gt;
&lt;li&gt;production structure  &lt;/li&gt;
&lt;li&gt;open source&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Build faster. Focus on features.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/14585/how-to-deploy-a-production-ready-file-server-on-a-vps-for-free</guid>
<pubDate>Sun, 12 Apr 2026 16:42:31 +0000</pubDate>
</item>
<item>
<title> All Spring Boot Annotations — One Cheat Sheet</title>
<link>https://coderlegion.com/12371/all-spring-boot-annotations-one-cheat-sheet</link>
<description>&lt;p&gt;See the link&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/12371/all-spring-boot-annotations-one-cheat-sheet</guid>
<pubDate>Wed, 04 Mar 2026 16:40:49 +0000</pubDate>
</item>
<item>
<title>Building Production-Ready Spring Boot Microservices: Lessons from Scaling to Millions</title>
<link>https://coderlegion.com/11476/building-production-ready-spring-boot-microservices-lessons-from-scaling-to-millions</link>
<description>&lt;h3 id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;Over the past several years working in e-commerce, I've architected and scaled microservices handling millions of customer requests daily, including platforms processing over $100M in transactions. In this article, I'll share practical patterns for building Spring Boot microservices that can handle production scale.&lt;/p&gt;
&lt;p&gt;This isn't theory—these are battle-tested approaches proven under real-world load.&lt;/p&gt;
&lt;h3 id=&quot;1-service-boundaries-and-domain-driven-design&quot;&gt;1. Service Boundaries and Domain-Driven Design&lt;/h3&gt;
&lt;p&gt;The biggest mistake teams make is creating microservices that are too granular or tightly coupled.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key principle:&lt;/strong&gt; Each service should own a complete business capability.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// ❌ BAD: Services that are too granular
CustomerNameService
CustomerAddressService

// ✅ GOOD: Services that own complete domains
CustomerProfileService // owns all customer identity/profile data
SubscriptionService    // owns subscriptions, benefits, rewards
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When building a subscription platform, a &lt;code&gt;SubscriptionService&lt;/code&gt; should own the complete domain: enrollment, benefits tracking, payment integration, and conversions. This prevents the &quot;distributed monolith&quot; where you call 10 services for one operation.&lt;/p&gt;
&lt;h3 id=&quot;2-handling-distributed-transactions-with-saga-pattern&quot;&gt;2. Handling Distributed Transactions with Saga Pattern&lt;/h3&gt;
&lt;p&gt;At scale, distributed transactions become your enemy. Here's the orchestration approach:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Service
public class SubscriptionEnrollmentOrchestrator {
    
    @Transactional
    public EnrollmentResult enrollMember(EnrollmentRequest request) {
        // Step 1: Validate customer (synchronous)
        CustomerProfile customer = customerClient.getCustomer(request.getCustomerId())
            .orElseThrow(() -&amp;gt; new CustomerNotFoundException());
            
        // Step 2: Process payment (synchronous with compensation)
        PaymentResult payment;
        try {
            payment = paymentClient.processPayment(request.getPaymentMethod());
        } catch (PaymentException e) {
            throw new EnrollmentFailedException(&quot;Payment failed&quot;, e);
        }
        
        // Step 3: Create subscription (local transaction)
        Subscription subscription;
        try {
            subscription = createSubscription(customer, payment);
            subscriptionRepository.save(subscription);
        } catch (Exception e) {
            // Compensate payment
            paymentClient.refundPayment(payment.getTransactionId());
            throw new EnrollmentFailedException(&quot;Subscription creation failed&quot;, e);
        }
        
        // Step 4: Publish event for async operations
        eventPublisher.publish(new SubscriptionEnrolledEvent(subscription));
        
        return EnrollmentResult.success(subscription);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Key lessons:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Make operations idempotent&lt;/strong&gt; - Include transaction IDs so retries don't cause duplicates&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compensate explicitly&lt;/strong&gt; - If step N fails, undo steps 1 through N-1&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Separate critical path from eventual consistency&lt;/strong&gt; - Payment is synchronous, emails are async&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;3-strategic-caching-for-high-traffic-systems&quot;&gt;3. Strategic Caching for High-Traffic Systems&lt;/h3&gt;
&lt;p&gt;We reduced database load by 85% through strategic caching:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Service
public class SubscriptionService {
    
    private final LoadingCache&amp;lt;String, Optional&amp;lt;Subscription&amp;gt;&amp;gt; cache;
    
    public SubscriptionService(SubscriptionRepository repository) {
        this.cache = CacheBuilder.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .recordStats()
            .build(CacheLoader.from(repository::findByCustomerId));
    }
    
    @Transactional
    public Subscription updateSubscription(String id, SubscriptionUpdate update) {
        Subscription updated = repository.findById(id)
            .map(s -&amp;gt; applyUpdate(s, update))
            .orElseThrow(() -&amp;gt; new SubscriptionNotFoundException(id));
            
        repository.save(updated);
        cache.invalidate(updated.getCustomerId()); // Invalidate on write
        
        return updated;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Caching layers:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Application-level (Guava/Caffeine) - 5-minute TTL for hot data&lt;/li&gt;
&lt;li&gt;Redis distributed cache - 15-minute TTL for cross-instance consistency&lt;/li&gt;
&lt;li&gt;Database with proper indexing&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;4-circuit-breakers-and-resilience&quot;&gt;4. Circuit Breakers and Resilience&lt;/h3&gt;
&lt;p&gt;When calling other services, assume they will fail:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Service
public class CustomerProfileClient {
    
    @CircuitBreaker(name = &quot;customer-profile&quot;, fallbackMethod = &quot;getCustomerFallback&quot;)
    @TimeLimiter(name = &quot;customer-profile&quot;)
    public CompletableFuture&amp;lt;CustomerProfile&amp;gt; getCustomer(String customerId) {
        return CompletableFuture.supplyAsync(() -&amp;gt; 
            restTemplate.getForObject(&quot;/api/customers/&quot; + customerId, CustomerProfile.class)
        );
    }
    
    private CompletableFuture&amp;lt;CustomerProfile&amp;gt; getCustomerFallback(String customerId, Exception ex) {
        log.warn(&quot;Customer service unavailable, using cached data&quot;);
        return CompletableFuture.completedFuture(
            customerCache.get(customerId)
                .orElseThrow(() -&amp;gt; new ServiceUnavailableException(&quot;No cached data&quot;, ex))
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Configuration:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;CircuitBreakerConfig.custom()
    .failureRateThreshold(50)          // Open if 50% fail
    .waitDurationInOpenState(Duration.ofSeconds(30))
    .slidingWindowSize(100)            // Consider last 100 calls
    .slowCallDurationThreshold(Duration.ofSeconds(2))
    .build();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;5-structured-logging-and-distributed-tracing&quot;&gt;5. Structured Logging and Distributed Tracing&lt;/h3&gt;
&lt;p&gt;You can't fix what you can't see:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Slf4j
@RestController
public class SubscriptionController {
    
    @PostMapping(&quot;/subscriptions&quot;)
    public ResponseEntity&amp;lt;SubscriptionResponse&amp;gt; enrollMember(
            @RequestBody EnrollmentRequest request,
            @RequestHeader(&quot;X-Request-ID&quot;) String requestId) {
        
        MDC.put(&quot;requestId&quot;, requestId);
        MDC.put(&quot;customerId&quot;, request.getCustomerId());
        
        try {
            log.info(&quot;Starting enrollment&quot;, 
                kv(&quot;tier&quot;, request.getTier()),
                kv(&quot;paymentMethod&quot;, request.getPaymentMethod().getType()));
            
            Instant start = Instant.now();
            EnrollmentResult result = enrollmentService.enroll(request);
            
            log.info(&quot;Enrollment completed&quot;,
                kv(&quot;subscriptionId&quot;, result.getSubscriptionId()),
                kv(&quot;durationMs&quot;, Duration.between(start, Instant.now()).toMillis()));
            
            return ResponseEntity.ok(toResponse(result));
        } finally {
            MDC.clear();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Logging standards:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use structured key-value pairs, not string concatenation&lt;/li&gt;
&lt;li&gt;Track request IDs across service boundaries&lt;/li&gt;
&lt;li&gt;Include customer ID and operation type in every log&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;6-database-query-optimization&quot;&gt;6. Database Query Optimization&lt;/h3&gt;
&lt;p&gt;Slow queries kill scalability:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Entity
@Table(name = &quot;subscriptions&quot;, indexes = {
    @Index(name = &quot;idx_customer_id&quot;, columnList = &quot;customer_id&quot;),
    @Index(name = &quot;idx_tier_status&quot;, columnList = &quot;tier, status&quot;),
    @Index(name = &quot;idx_expires_at&quot;, columnList = &quot;expires_at&quot;)
})
public class Subscription {
    // Use @BatchSize to prevent N+1 queries
    @OneToMany(mappedBy = &quot;subscription&quot;, fetch = FetchType.LAZY)
    @BatchSize(size = 50)
    private List&amp;lt;Benefit&amp;gt; benefits;
}

@Repository
public interface SubscriptionRepository extends JpaRepository&amp;lt;Subscription, String&amp;gt; {
    
    // Custom query with fetch join to avoid N+1
    @Query(&quot;SELECT s FROM Subscription s LEFT JOIN FETCH s.benefits WHERE s.customerId = :customerId&quot;)
    Optional&amp;lt;Subscription&amp;gt; findByCustomerIdWithBenefits(@Param(&quot;customerId&quot;) String customerId);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Performance lessons:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Index everything you query by&lt;/li&gt;
&lt;li&gt;Use batch fetching to prevent N+1 queries&lt;/li&gt;
&lt;li&gt;Paginate large results&lt;/li&gt;
&lt;li&gt;Monitor and alert on queries &amp;gt;100ms&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;7-connection-pool-tuning&quot;&gt;7. Connection Pool Tuning&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;spring:
  datasource:
    hikari:
      # Formula: (core_count * 2) + effective_spindle_count
      # For 4-core with SSD: (4 * 2) + 1 = 9
      maximum-pool-size: 10
      minimum-idle: 5
      connection-timeout: 3000
      validation-timeout: 1000
      leak-detection-threshold: 60000
      max-lifetime: 1800000  # 30 minutes
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;8-zero-downtime-deployments&quot;&gt;8. Zero-Downtime Deployments&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: subscription-service
spec:
  replicas: 6
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 3        # Add 50% extra during update
      maxUnavailable: 1  # Keep 5/6 running
  template:
    spec:
      containers:
      - name: subscription-service
        readinessProbe:
          httpGet:
            path: /actuator/health/readiness
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 5
        livenessProbe:
          httpGet:
            path: /actuator/health/liveness
            port: 8080
          initialDelaySeconds: 60
          periodSeconds: 10
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;9-metrics-that-matter&quot;&gt;9. Metrics That Matter&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Service
public class MetricsService {
    
    private final Counter enrollmentsCounter;
    private final Timer enrollmentDuration;
    
    public MetricsService(MeterRegistry registry) {
        this.enrollmentsCounter = Counter.builder(&quot;subscription.enrollments&quot;)
            .tag(&quot;service&quot;, &quot;subscription&quot;)
            .register(registry);
            
        this.enrollmentDuration = Timer.builder(&quot;subscription.enrollment.duration&quot;)
            .publishPercentiles(0.5, 0.95, 0.99)
            .register(registry);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Monitor:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Business: Enrollments/hour, conversion rate, revenue&lt;/li&gt;
&lt;li&gt;Technical: P50/P95/P99 latency, error rate, throughput&lt;/li&gt;
&lt;li&gt;Infrastructure: CPU, memory, connection pool utilization&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;10-testing-strategy&quot;&gt;10. Testing Strategy&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// Unit tests - fast, isolated
@Test
void shouldCalculateCorrectExpirationDate() {
    EnrollmentService service = new EnrollmentService(
        mock(CustomerClient.class),
        mock(PaymentClient.class),
        mock(SubscriptionRepository.class)
    );
    
    Instant enrolledAt = Instant.parse(&quot;2024-01-01T00:00:00Z&quot;);
    Instant expected = Instant.parse(&quot;2025-01-01T00:00:00Z&quot;);
    
    assertEquals(expected, service.calculateExpiration(enrolledAt, SubscriptionTier.ANNUAL));
}

// Integration tests - with real database
@SpringBootTest
@Testcontainers
class SubscriptionServiceIntegrationTest {
    
    @Container
    static PostgreSQLContainer&amp;lt;?&amp;gt; postgres = new PostgreSQLContainer&amp;lt;&amp;gt;(&quot;postgres:15&quot;);
    
    @Test
    void shouldPersistAndRetrieveSubscription() {
        EnrollmentResult result = service.enroll(request);
        Optional&amp;lt;Subscription&amp;gt; retrieved = service.getSubscription(result.getSubscriptionId());
        
        assertTrue(retrieved.isPresent());
        assertEquals(SubscriptionTier.BASIC, retrieved.get().getTier());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;key-takeaways&quot;&gt;Key Takeaways&lt;/h3&gt;
&lt;p&gt;After building and scaling microservices handling millions of requests:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Design for failure&lt;/strong&gt; - Circuit breakers, timeouts, and fallbacks are not optional&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Embrace eventual consistency&lt;/strong&gt; - Not everything needs to be synchronous&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Observability is critical&lt;/strong&gt; - You can't improve what you can't measure&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cache aggressively&lt;/strong&gt; - But invalidate intelligently&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test at every level&lt;/strong&gt; - Unit, integration, contract, and load tests&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automate deployments&lt;/strong&gt; - Including rollbacks and scaling&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance is a feature&lt;/strong&gt; - Optimize from day one&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;what-s-next-&quot;&gt;What's Next?&lt;/h3&gt;
&lt;p&gt;In future posts, I'll dive deeper into:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Advanced observability with distributed tracing&lt;/li&gt;
&lt;li&gt;Chaos engineering and resilience testing&lt;/li&gt;
&lt;li&gt;Cost optimization for cloud deployments&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Questions or want to discuss these patterns?&lt;/strong&gt; Leave a comment below or connect with me on &lt;a rel=&quot;nofollow&quot; href=&quot;https://linkedin.com/in/vamsithatikonda&quot;&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; #spring-boot #microservices #java #system-design #aws #architecture #scalability #production-engineering&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/11476/building-production-ready-spring-boot-microservices-lessons-from-scaling-to-millions</guid>
<pubDate>Thu, 12 Feb 2026 05:38:38 +0000</pubDate>
</item>
<item>
<title>Client side pagination is not good in some cases</title>
<link>https://coderlegion.com/4519/client-side-pagination-is-not-good-in-some-cases</link>
<description>&lt;h3 id=&quot;the-pagination-pitfall-a-react-spring-boot-cautionary-tale&quot;&gt;The Pagination Pitfall: A React + Spring Boot Cautionary Tale&lt;/h3&gt;
&lt;h4 id=&quot;the-setup-it-ll-be-fine-&quot;&gt;The Setup: &quot;It’ll Be Fine...&quot;&lt;/h4&gt;
&lt;p&gt;A few years ago, I was working on a dashboard application with a &lt;strong&gt;React frontend&lt;/strong&gt; and a &lt;strong&gt;Spring Boot backend&lt;/strong&gt;. The task seemed simple: display a list of users with pagination.&lt;/p&gt;
&lt;p&gt;One of the newer devs suggested:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Let’s just fetch everything once and paginate on the client side — it’ll be faster for the user.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We had maybe 500 users at the time, so… sure. Why not?&lt;/p&gt;
&lt;p&gt;And then we shipped it.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;the-plot-twist-growth-happens&quot;&gt;The Plot Twist: Growth Happens&lt;/h4&gt;
&lt;p&gt;A few months passed.&lt;/p&gt;
&lt;p&gt;Marketing launched a campaign that worked &lt;strong&gt;too well&lt;/strong&gt;.&lt;br&gt;
Suddenly, instead of 500 users, we had &lt;strong&gt;250,000+&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The dashboard became unusable. The browser would freeze, mobile devices would crash, and CPU usage would spike through the roof.&lt;br&gt;
Our once-smooth user experience turned into a memory-leaking nightmare.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;-the-mistake-client-side-pagination-at-scale&quot;&gt;⚠️ The Mistake: Client-Side Pagination at Scale&lt;/h4&gt;
&lt;p&gt;Client-side pagination works &lt;strong&gt;only&lt;/strong&gt; when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The dataset is small and controlled  &lt;/li&gt;
&lt;li&gt;The entire dataset can be loaded efficiently in the browser  &lt;/li&gt;
&lt;li&gt;You don’t expect explosive growth  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But when you're working with &lt;strong&gt;Spring Boot APIs&lt;/strong&gt; and &lt;strong&gt;React&lt;/strong&gt;, it's tempting to fetch all data via a single REST call, dump it into state, and let React handle the UI slicing.&lt;/p&gt;
&lt;p&gt;What’s wrong with that?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You're sending huge payloads over the wire  &lt;/li&gt;
&lt;li&gt;You're increasing memory usage on the frontend  &lt;/li&gt;
&lt;li&gt;You’re locking users into long initial load times  &lt;/li&gt;
&lt;li&gt;You make updates or filtering much harder  &lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;-the-fix-server-side-pagination-with-spring-boot-react&quot;&gt;✅ The Fix: Server-Side Pagination with Spring Boot + React&lt;/h4&gt;
&lt;p&gt;We refactored. Here’s what we did:&lt;/p&gt;
&lt;h5 id=&quot;backend-spring-boot-&quot;&gt;Backend (Spring Boot)&lt;/h5&gt;
&lt;p&gt;We used &lt;code&gt;Pageable&lt;/code&gt; from Spring Data:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@GetMapping(&quot;/users&quot;)
public Page&amp;lt;User&amp;gt; getUsers(Pageable pageable) {
    return userRepository.findAll(pageable);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Spring Boot handles page number, size, sorting, and total count under the hood.&lt;/p&gt;
&lt;h5 id=&quot;frontend-react-&quot;&gt;Frontend (React)&lt;/h5&gt;
&lt;p&gt;We called the API with query parameters like &lt;code&gt;?page=0&amp;amp;size=20&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const fetchUsers = async (page, size) =&amp;gt; {
  const res = await fetch(`/api/users?page=${page}&amp;amp;size=${size}`);
  const data = await res.json();
  setUsers(data.content);
  setTotalPages(data.totalPages);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We then plugged this into a pagination UI. Now, we load &lt;strong&gt;only what we need&lt;/strong&gt;, keep response times fast, and keep the browser happy.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;lessons-learned&quot;&gt;Lessons Learned&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;React + Spring Boot&lt;/strong&gt; is a powerful combo — but scale with caution  &lt;/li&gt;
&lt;li&gt;Don’t optimize for “today’s data” unless you’re &lt;em&gt;certain&lt;/em&gt; it won’t grow  &lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;Spring’s &lt;code&gt;Pageable&lt;/code&gt;&lt;/strong&gt; and let the database do the heavy lifting  &lt;/li&gt;
&lt;li&gt;Keep network traffic and frontend memory usage lean  &lt;/li&gt;
&lt;li&gt;Premature optimization is bad, but premature &lt;em&gt;de-optimization&lt;/em&gt; is worse  &lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;tl-dr&quot;&gt;TL;DR&lt;/h4&gt;
&lt;p&gt;Using client-side pagination for large or unknown datasets is like bringing a spoon to a firefight.&lt;/p&gt;
&lt;p&gt;It might work — for a while — but you’re setting yourself up for disaster.&lt;/p&gt;
&lt;p&gt; Use the right tools. Paginate smart. Scale with confidence.&lt;/p&gt;
</description>
<category>Java</category>
<guid isPermaLink="true">https://coderlegion.com/4519/client-side-pagination-is-not-good-in-some-cases</guid>
<pubDate>Fri, 08 Aug 2025 20:29:08 +0000</pubDate>
</item>
</channel>
</rss>