<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Piyush Kumar Singh</title>
    <description>The latest articles on DEV Community by Piyush Kumar Singh (@piyushsingh_dev).</description>
    <link>https://dev.to/piyushsingh_dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3909333%2Fe0081e0c-5341-42fe-b711-92dafd3e0fdb.jpg</url>
      <title>DEV Community: Piyush Kumar Singh</title>
      <link>https://dev.to/piyushsingh_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/piyushsingh_dev"/>
    <language>en</language>
    <item>
      <title>Method-Level Security in Spring Security — How @PreAuthorize Actually Works Inside</title>
      <dc:creator>Piyush Kumar Singh</dc:creator>
      <pubDate>Mon, 20 Jul 2026 08:19:11 +0000</pubDate>
      <link>https://dev.to/piyushsingh_dev/method-level-security-in-spring-security-how-preauthorize-actually-works-inside-7k</link>
      <guid>https://dev.to/piyushsingh_dev/method-level-security-in-spring-security-how-preauthorize-actually-works-inside-7k</guid>
      <description>&lt;p&gt;In the first three parts of this series, every security decision happened before your controller ran.&lt;/p&gt;

&lt;p&gt;The filter chain intercepted the HTTP request. JWT validation proved who you were. OAuth2 handed off your identity from Google. All of it — every authentication check, every URL-level access rule — happened at the HTTP boundary, before a single line of your business logic executed.&lt;/p&gt;

&lt;p&gt;That’s a solid foundation. But it has a gap that URL patterns can’t fill.&lt;/p&gt;

&lt;p&gt;Consider this: &lt;strong&gt;.requestMatchers("/invoices/&lt;/strong&gt;").hasRole("USER")** Lets any authenticated user hit any invoice endpoint. But what if User A should only see their own invoices, not User B's? The URL pattern /invoices/4521 doesn't know who created invoice 4521. You need to be inside the method, with access to the arguments or the return value, to make that ownership decision.&lt;/p&gt;

&lt;p&gt;That’s the problem method-level security solves. And it’s why it exists as a completely separate layer from the filter chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Enabling it — and why it’s off by default&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Method-level security is disabled by default. You have to opt in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="nd"&gt;@EnableMethodSecurity&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SecurityConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// rest of your config&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why off by default? Because enabling it causes Spring to create AOP proxies around every bean with a security annotation. That adds startup overhead and a small per-call interception cost. If you don’t need it, you don’t pay for it. Spring’s design philosophy is explicit opt-in for features that have a cost.&lt;/p&gt;

&lt;p&gt;One thing worth knowing if you’re reading older code: @EnableMethodSecurity replaced @EnableGlobalMethodSecurity(prePostEnabled = true) in Spring Security 6. If you see the old annotation in a codebase, it still works, but it's deprecated. Don't use it for new projects — the behaviour is slightly different, and the new API is cleaner.&lt;/p&gt;

&lt;p&gt;**&lt;/p&gt;

&lt;h2&gt;
  
  
  What Spring actually does when it sees @PreAuthorize
&lt;/h2&gt;

&lt;p&gt;**&lt;br&gt;
This is where most tutorials stop saying anything useful. They show you the annotation and assume you understand what fires it.&lt;/p&gt;

&lt;p&gt;Here’s what actually happens.&lt;/p&gt;

&lt;p&gt;When your Spring application starts and finds a bean with @PreAuthorize on a method, Spring doesn't modify your class. It creates a proxy object that wraps your bean. From that point on, any other bean in your application that injects InvoiceService actually receives the proxy, not the real service.&lt;/p&gt;

&lt;p&gt;When something calls invoiceService.getInvoice(id), the call hits the proxy first. The proxy hands it to *&lt;em&gt;AuthorizationManagerBeforeMethodInterceptor *&lt;/em&gt;— an AOP advice registered specifically to handle @PreAuthorize. This interceptor is the component that actually does the security check.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn7d0oz3t91k0xq690m7w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn7d0oz3t91k0xq690m7w.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The interceptor goes to SecurityContextHolder and retrieves the current Authentication object. If you read Part 2 of this series, this is the same Authentication that your JWT filter stored there. If you read Part 3, it's the same one the OAuth2 login flow stored there. The filter chain populates it. Method security reads it. Same object, same store, different consumers.&lt;/p&gt;

&lt;p&gt;The interceptor then evaluates the SpEL expression — the string inside the annotation — against that authentication. If it returns true, control passes to your real method. If it returns false, AccessDeniedException is thrown, caught by ExceptionTranslationFilter (from Part 1), and the client receives a 403.&lt;/p&gt;

&lt;p&gt;Your actual service code only runs if all of that passes cleanly.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;SpEL expressions — what you can actually write&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The string inside @PreAuthorize is a Spring Expression Language (SpEL) expression evaluated at runtime. It has access to the current authentication, method arguments, and Spring beans.&lt;/p&gt;

&lt;p&gt;The expressions you’ll use most often in production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;
&lt;span class="c1"&gt;// Check for a role — Spring prepends "ROLE_" automatically&lt;/span&gt;
&lt;span class="c1"&gt;// This checks for ROLE_ADMIN, not ADMIN&lt;/span&gt;
&lt;span class="nd"&gt;@PreAuthorize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hasRole('ADMIN')"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;deleteUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Check for a specific authority - no prefix added&lt;/span&gt;
&lt;span class="c1"&gt;// Use this for fine-grained permissions&lt;/span&gt;
&lt;span class="nd"&gt;@PreAuthorize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hasAuthority('PAYMENT_WRITE')"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;processPayment&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Payment&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Access method arguments - the # prefix refers to parameter names&lt;/span&gt;
&lt;span class="c1"&gt;// This is the ownership check URL patterns can't do&lt;/span&gt;
&lt;span class="nd"&gt;@PreAuthorize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"#userId == authentication.principal.id"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Invoice&lt;/span&gt; &lt;span class="nf"&gt;getInvoice&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;invoiceId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Call a Spring bean from inside the expression&lt;/span&gt;
&lt;span class="c1"&gt;// @ prefix followed by bean name&lt;/span&gt;
&lt;span class="nd"&gt;@PreAuthorize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"@invoiceService.isOwner(#id, authentication.name)"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Invoice&lt;/span&gt; &lt;span class="nf"&gt;getInvoice&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference between hasRole and hasAuthority trips up almost every developer at least once. hasRole('ADMIN') checks for the authority ROLE_ADMIN — Spring prepends the prefix automatically. hasAuthority('PAYMENT_WRITE') checks for the exact string. If you store custom permissions in your authorities list without the ROLE_ prefix, use hasAuthority. If you're working with standard roles granted via ROLE_ conventions, use hasRole.&lt;/p&gt;

&lt;p&gt;The #userId == authentication.principal.id pattern is the most valuable thing method security enables. You can't express "the requested userId matches the logged-in user's id" at the URL-matcher level — but you can express it right here, next to the method that enforces it. That's both more secure (the check is right next to the code it protects) and more readable.&lt;/p&gt;

&lt;p&gt;**&lt;/p&gt;

&lt;h2&gt;
  
  
  @PostAuthorize — when you need the return value
&lt;/h2&gt;

&lt;p&gt;**&lt;br&gt;
@PostAuthorize runs after the method executes. It has access to the return value through the returnObject keyword:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Only return the invoice if the logged-in user owns it&lt;/span&gt;
&lt;span class="nd"&gt;@PostAuthorize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"returnObject.ownerId == authentication.principal.id"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Invoice&lt;/span&gt; &lt;span class="nf"&gt;getInvoice&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;invoiceRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The use case: you need to fetch a record from the database to know who owns it. You don’t want to run a separate ownership query before the fetch — that’s two queries instead of one. So you fetch it, then check.&lt;/p&gt;

&lt;p&gt;The gotcha — and this one matters: the method always runs. If it has side effects — writing to a database, sending an email, calling an external service — those side effects happen even if @PostAuthorize denies access afterward. Only use @PostAuthorize on read-only operations where running the method and then rejecting the result is safe.&lt;/p&gt;

&lt;p&gt;**&lt;/p&gt;

&lt;h2&gt;
  
  
  Where method security fits — the full picture
&lt;/h2&gt;

&lt;p&gt;**&lt;br&gt;
Parts 1 through 4 have built a three-layer security system. This diagram shows how they connect.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2slq09gyiq7w81zur5g9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2slq09gyiq7w81zur5g9.png" alt=" "&gt;&lt;/a&gt;&lt;br&gt;
Layer 1 is the filter chain. It operates at the HTTP boundary, before your controller. FilterChainProxy picks the right SecurityFilterChain. AuthorizationFilter applies URL-level access rules. Requests that fail here never reach your code.&lt;/p&gt;

&lt;p&gt;Layer 2 is authentication. JWT validation via OncePerRequestFilter (Part 2) or OAuth2 login via OAuth2LoginAuthenticationFilter (Part 3) — both populate SecurityContextHolder with the verified identity. This is the layer that answers "who is this person."&lt;/p&gt;

&lt;p&gt;Layer 3 is method security. The AOP proxy intercepts service-layer method calls. @PreAuthorize Reads from the same SecurityContextHolder that Layer 2 populated. Fine-grained, data-aware decisions happen here — decisions that Layer 1 couldn't make because they depend on method arguments or return values, not just URL patterns.&lt;/p&gt;

&lt;p&gt;All three layers share one SecurityContextHolder. Layer 2 writes to it. Layers 1 and 3 read from it. The filter chain and method security are not two separate security systems bolted together. They're two consumers of the same authentication store, operating at different points in the request lifecycle.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;@Secured and @RolesAllowed — what they are and why @PreAuthorize wins&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Two older annotations you’ll encounter in legacy code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Spring's older annotation — no SpEL, role checks only&lt;/span&gt;
&lt;span class="nd"&gt;@Secured&lt;/span&gt;&lt;span class="o"&gt;({&lt;/span&gt;&lt;span class="s"&gt;"ROLE_ADMIN"&lt;/span&gt;&lt;span class="o"&gt;})&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;deleteUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// JSR-250 standard - not Spring-specific, same limitations&lt;/span&gt;
&lt;span class="nd"&gt;@RolesAllowed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ADMIN"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;deleteUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;@Secured is Spring-specific, no SpEL. @RolesAllowed is the JSR-250 standard, also no SpEL. Both work for simple role checks. Neither lets you access method arguments, call Spring beans, or check return values.&lt;/p&gt;

&lt;p&gt;@PreAuthorize With full SpEL support does everything both of those do and more. For new code, there's no reason to use either of the older annotations. Enable @EnableMethodSecurity and use @PreAuthorize.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;When NOT to use method-level security — and the proxy trap&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The self-invocation proxy trap is the most common @PreAuthorize bug in production.&lt;/p&gt;

&lt;p&gt;If a method inside a bean calls another annotated method in the same bean, the proxy is bypassed. The call goes directly to the real object, skipping the AOP interceptor entirely. The @PreAuthorize annotation silently does nothing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InvoiceService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;processAll&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// This calls the REAL method directly - not the proxy&lt;/span&gt;
        &lt;span class="c1"&gt;// @PreAuthorize on getInvoice will NOT fire&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInvoice&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1L&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="nd"&gt;@PreAuthorize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hasRole('ADMIN')"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Invoice&lt;/span&gt; &lt;span class="nf"&gt;getInvoice&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;invoiceRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix: inject the service into itself via Spring (which gives you the proxy), or move the annotated method to a different bean.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don’t put complex ownership logic in SpEL.&lt;/strong&gt; Calling @userService.isOwner(#id, authentication.name) from inside an annotation is hard to test, hard to debug, and invisible during code reviews. If the expression is more than one condition, extract it into a properly tested method.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don’t use @PostAuthorize on methods with side effects.&lt;/strong&gt; The method runs regardless. If you can’t afford the side effect when access is denied, use @PreAuthorize with a pre-fetch ownership check instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Method security doesn’t replace the filter chain.&lt;/strong&gt; Both layers are needed. The filter chain handles unauthenticated requests — turning them away before they reach any code at all. Method security handles authorisation decisions that require data context the filter chain doesn’t have. They’re complementary layers of the same system, not alternatives to each other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The series in three sentences&lt;/strong&gt;&lt;br&gt;
The filter chain decides if you can enter the building. JWT or OAuth2 proves who you are at the door. @PreAuthorize decides what rooms you can open once you're inside. Same security context. Three different places it applies.&lt;/p&gt;

</description>
      <category>springsecurity</category>
      <category>java</category>
      <category>springboot</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>How Kafka Actually Works — Logs, Partitions, and the Design Decisions Nobody Explains</title>
      <dc:creator>Piyush Kumar Singh</dc:creator>
      <pubDate>Tue, 07 Jul 2026 09:58:16 +0000</pubDate>
      <link>https://dev.to/piyushsingh_dev/how-kafka-actually-works-logs-partitions-and-the-design-decisions-nobody-explains-f43</link>
      <guid>https://dev.to/piyushsingh_dev/how-kafka-actually-works-logs-partitions-and-the-design-decisions-nobody-explains-f43</guid>
      <description>&lt;p&gt;The first time I used Kafka in production, I treated it like RabbitMQ: a message queue. Produce a message, consumer picks it up, done. The message disappears. That’s how queues work. &lt;br&gt;
That mental model worked — until a downstream payment processor went down for 3 hours, and we had to replay everything it missed. I went to look for the messages.&lt;/p&gt;

&lt;p&gt;They were still there.&lt;br&gt;
That moment is when Kafka actually clicked for me. It is not a queue. The messages do not disappear when consumed. And once that distinction lands, every other design decision Kafka makes becomes logical rather than arbitrary.&lt;/p&gt;
&lt;h2&gt;
  
  
  The commit log — Kafka’s actual storage model
&lt;/h2&gt;

&lt;p&gt;Kafka stores messages in an append-only commit log — a file on disk that only ever grows. Every message gets an offset: a sequential position number starting at zero. When a consumer reads a message, nothing is deleted. The message stays. The consumer’s position pointer simply advances.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0noausx2kgwr8i3mgpd2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0noausx2kgwr8i3mgpd2.png" alt=" " width="800" height="396"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is why two completely independent systems can read the same Kafka topic without interfering with each other. Consumer Group A might be at offset 7. Consumer Group B might be at offset 4. They share the same underlying log but track their own positions independently. If you add a third consumer group next week — a new analytics pipeline, a new audit service — it starts at offset 0 and replays everything from the beginning.&lt;/p&gt;

&lt;p&gt;In a traditional message queue, that’s impossible. The message was already consumed and discarded. In Kafka, the log is the source of truth, and consumers are just readers moving through it at their own pace.&lt;/p&gt;

&lt;p&gt;Now here’s the counterintuitive part. Kafka writes to disk and is still faster than many in-memory message brokers for high throughput. The reason is sequential I/O. Random disk writes are slow — the write head has to seek across the platter. Sequential writes, appending to the end of a file, are fast. Kafka only ever appends. Combined with the OS page cache — which keeps recently written data in RAM automatically — and a zero-copy technique called sendfile() that transfers data directly from the page cache to the network socket without passing through application memory, Kafka achieves throughput that can reach hundreds of thousands of messages per second on commodity hardware.&lt;/p&gt;

&lt;p&gt;The disk is not Kafka’s compromise. It is the design.&lt;/p&gt;
&lt;h2&gt;
  
  
  Topics, partitions, and the order guarantee everyone gets wrong
&lt;/h2&gt;

&lt;p&gt;A topic is a named log — think of it like a database table, but for events. A partition is a shard of that topic stored on a specific broker. More partitions means more parallelism, which means higher throughput.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpm6zb2rlb9imr8c2dtol.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpm6zb2rlb9imr8c2dtol.png" alt=" " width="800" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is the part that trips up almost every developer building on Kafka for the first time: Kafka guarantees message order within a partition. It does not guarantee order across partitions.&lt;/p&gt;

&lt;p&gt;If you have a topic with three partitions and a producer sends ten messages for the same user, those messages could land on any partition — round-robin by default. If they scatter across partitions, a consumer might process them out of order.&lt;/p&gt;

&lt;p&gt;The fix is the partition key. You set the message key to something stable — a user ID, an order ID, a transaction ID. Kafka hashes that key and routes all messages with the same key to the same partition, every time. Every message for user-1001 lands on partition 0. Every message for user-2002 lands on partition 1. Order is guaranteed per user, not globally — which is usually exactly what you need.&lt;/p&gt;

&lt;p&gt;This is one of the most important production decisions you make when designing a Kafka-based system, and most tutorials mention it in a single sentence. Getting it wrong means debugging ordering issues at 2 am that only manifest under load, when multiple partitions are being consumed concurrently.&lt;/p&gt;
&lt;h2&gt;
  
  
  Consumer groups: how Kafka scales consumption
&lt;/h2&gt;

&lt;p&gt;A consumer group is a set of consumers that share the work of reading a topic. Kafka assigns each partition to exactly one consumer in the group at any given time. With three partitions and three consumers, each consumer handles one partition. With three partitions and five consumers, two consumers sit idle — you cannot have more active consumers than partitions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgkl7iggb7ev5qqes2096.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgkl7iggb7ev5qqes2096.png" alt=" " width="799" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each consumer group tracks its own offset for each partition, stored in an internal Kafka topic called __consumer_offsets. When a consumer restarts after a crash, it reads its last committed offset from that topic and resumes exactly where it left off. This is what makes Kafka fault-tolerant — not replication alone, but the combination of durable storage and tracked offsets.&lt;/p&gt;

&lt;p&gt;The production pain point is rebalancing. When a consumer joins or leaves a group — whether through a deployment, a crash, or a scale event — Kafka reassigns partitions across the remaining consumers. During a rebalance, consumption pauses across the entire group. For most workloads, this pause lasts a few seconds. For high-throughput systems with many consumers and frequent deployments, rebalancing can become a significant source of latency spikes — sometimes called a “rebalancing storm.”&lt;/p&gt;

&lt;p&gt;Kafka 4.0’s KIP-848 incremental cooperative rebalancing dramatically reduces this. Instead of stopping all consumers and reassigning all partitions, consumers now transfer partitions incrementally while other consumers continue reading. If you’re seeing rebalancing issues in an older Kafka version, upgrading is one of the most effective fixes available.&lt;/p&gt;
&lt;h2&gt;
  
  
  Replication — how Kafka survives broker failures
&lt;/h2&gt;

&lt;p&gt;Each partition has one leader broker and zero or more follower brokers. All reads and writes go to the leader. Followers replicate the leader’s log passively, staying as close to current as possible. If the leader broker fails, Kafka elects a new leader from the in-sync replica set (ISR) — the brokers that are fully caught up with the leader.&lt;/p&gt;

&lt;p&gt;The replication factor controls how many copies exist. A factor of three means you can lose two brokers and still serve traffic. Most production setups use three.&lt;/p&gt;

&lt;p&gt;The acks producer setting is your durability dial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Fire and forget — fastest, but you can lose messages
props.put("acks", "0");

// Wait for leader only — fast, safer
props.put("acks", "1");

// Wait for all in-sync replicas — slowest, safest
props.put("acks", "all");
props.put("min.insync.replicas", "2");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In fintech systems, acks=all with min.insync.replicas=2 is the standard. You are waiting for at least two brokers to confirm the write before acknowledging the producer. The added latency — typically 10–30ms — is a fair price for knowing that a single broker failure cannot silently drop a payment event.&lt;/p&gt;

&lt;h2&gt;
  
  
  KRaft — ZooKeeper is finally gone
&lt;/h2&gt;

&lt;p&gt;Before Kafka 4.0, every Kafka cluster depended on ZooKeeper — a separate distributed coordination service — to manage broker metadata, leader elections, and cluster configuration. This meant running and maintaining a separate system alongside Kafka, with its own failure modes and operational complexity.&lt;/p&gt;

&lt;p&gt;In March 2025, Apache Kafka 4.0 shipped. ZooKeeper is gone. KRaft — Kafka’s built-in Raft-based consensus protocol — now manages all cluster metadata directly on a quorum of controller nodes. Simpler deployments, faster failover, fewer moving parts.&lt;/p&gt;

&lt;p&gt;If you are setting up a new Kafka cluster today, there is no ZooKeeper to configure. If you are still running an older version with ZooKeeper, the migration path to KRaft is well-documented, and the upgrade is worth the effort. Any tutorial or architecture diagram that still shows ZooKeeper as a required component is outdated.&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to use Kafka
&lt;/h2&gt;

&lt;p&gt;This is the part that saves you from over-engineering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don’t use Kafka as a simple job queue&lt;/strong&gt;. If you need tasks processed once by one worker — send an email, resize an image, process a webhook — RabbitMQ or Amazon SQS is simpler and operationally lighter. Kafka’s complexity is only justified when you need replay, multiple independent consumer groups, or throughput above what a traditional queue can handle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don’t use Kafka for request-reply patterns&lt;/strong&gt;. Kafka is designed for one-way event streaming. If you need a response to a specific message, you are working against the grain. Use a proper RPC mechanism.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don’t get your partition count wrong at the start&lt;/strong&gt;. You can increase partitions later, but you cannot decrease them without recreating the topic. And increasing partitions changes the hash-to-partition mapping, which breaks the ordering guarantee for any keys that shift partitions. Plan your partition count upfront based on your expected throughput and consumer count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don’t skip idempotent producers&lt;/strong&gt;. Without enable.idempotence=true, network retries can produce duplicate messages. Kafka acknowledges a write, the network drops the response, your producer retries, and the message lands twice. In a payment system, that means double charges. Enable idempotence from day one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;props.put("enable.idempotence", "true");
props.put("acks", "all");
props.put("retries", Integer.MAX_VALUE);
props.put("max.in.flight.requests.per.connection", "5");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not optional in fintech. It is the baseline. Kafka is durable not despite writing to disk, but because of it. The commit log is not a compromise — it is the design. Once that clicks, every other Kafka decision makes sense.&lt;/p&gt;

</description>
      <category>kafka</category>
      <category>backend</category>
      <category>java</category>
      <category>springboot</category>
    </item>
    <item>
      <title>How OAuth2 Login Works in Spring Security — The Authorization Code Flow Explained</title>
      <dc:creator>Piyush Kumar Singh</dc:creator>
      <pubDate>Thu, 18 Jun 2026 15:55:32 +0000</pubDate>
      <link>https://dev.to/piyushsingh_dev/how-oauth2-login-works-in-spring-security-the-authorization-code-flow-explained-14l8</link>
      <guid>https://dev.to/piyushsingh_dev/how-oauth2-login-works-in-spring-security-the-authorization-code-flow-explained-14l8</guid>
      <description>&lt;p&gt;You were redirected to Google. You clicked Allow. You came back logged in. Between those three seconds, six things happened — and most backend developers who have built that button have never traced all of them.&lt;/p&gt;

&lt;p&gt;I know because I was one of them. When I first wired up OAuth2 login, it worked on the first attempt. I was relieved. Then a teammate asked me to explain what the state parameter was for, and I didn't have an answer. The feature worked. The understanding wasn’t there.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyzu2j05u6m1h94scmphs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyzu2j05u6m1h94scmphs.png" alt=" " width="799" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This article is that explanation — not the config, but the actual flow. What fires, in what order, and where it sits inside Spring Security’s filter chain. If you have read Part 1 of this series on Spring Security internals and Part 2 on JWT with OncePerRequestFilter, this picks up exactly where those left off.&lt;/p&gt;

&lt;h2&gt;
  
  
  OAuth2 is not what most developers think it is
&lt;/h2&gt;

&lt;p&gt;Before tracing the flow, one distinction is worth getting right.&lt;/p&gt;

&lt;p&gt;OAuth2 is an authorization framework, not an authentication protocol. It was designed to let applications access resources on behalf of users — not to verify who a user actually is. When you click “Login with Google,” OAuth2 handles the delegation part: your app is asking Google for permission to act on your behalf. But OAuth2 alone cannot tell your app who you are.&lt;/p&gt;

&lt;p&gt;That is what OpenID Connect (OIDC) does. OIDC is a thin identity layer built on top of OAuth2. It adds the id_token — a JWT that carries the user's identity information. When you use "Login with Google" in a Spring Boot app, you are using both: OAuth2 for the delegation mechanism, OIDC for the identity proof.&lt;/p&gt;

&lt;p&gt;Most developers conflate these two because the Spring Security config handles both transparently. Understanding the distinction matters when things go wrong — because the errors from each layer are very different.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 4 roles — mapped to something you already know
&lt;/h2&gt;

&lt;p&gt;Every OAuth2 explanation starts with four roles. Most explanations define them abstractly, which is why they don’t stick. Map them to Login with Google, and they’re immediately obvious.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbt5vra8gyh7rpleszl6c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbt5vra8gyh7rpleszl6c.png" alt=" " width="800" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resource Owner&lt;/strong&gt; — you, the user. You own the Google account and the data inside it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client&lt;/strong&gt; — your Spring Boot application. It wants access to the user’s basic profile on your behalf.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authorization Server&lt;/strong&gt; — Google’s accounts infrastructure (accounts.google.com). It authenticates the user, shows the consent screen, and issues tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resource Server&lt;/strong&gt; — Google’s userinfo API (googleapis.com). It holds the protected data — the email, name, and profile picture your app actually wants.&lt;/p&gt;

&lt;p&gt;One thing worth noting: in the Login with Google scenario, Google plays both the Authorization Server and the Resource Server. That is common with large identity providers. In your own systems — if you are building an API protected by OAuth2—your Spring Boot application becomes the Resource Server, and a separate authorization server (Keycloak, Okta, Auth0) handles the tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Authorization Code Flow—What Happened During That Redirect
&lt;/h2&gt;

&lt;p&gt;There are several OAuth2 flows. The one you use for web applications — and the only one worth knowing in 2026 — is the Authorization Code Flow. The others (Implicit, Resource Owner Password) are either deprecated or meant for machine-to-machine scenarios.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw7wpzclb1t5u77xemg0f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw7wpzclb1t5u77xemg0f.png" alt=" " width="800" height="682"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is each step:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — User clicks Login with Google.&lt;/strong&gt;&lt;br&gt;
Your Spring Boot app constructs a URL pointing to Google’s authorization endpoint, including your client_id, the redirect_uri it wants Google to return to, the scope (what access you're requesting — typically openid profile email), a state parameter (more on this shortly), and response_type=code. The user's browser is redirected to that URL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 — Google shows the consent screen.&lt;/strong&gt;&lt;br&gt;
Google authenticates the user (if not already logged in) and shows them what your app is requesting access to. The user clicks Allow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3 — Google redirects back with a code.&lt;/strong&gt;&lt;br&gt;
Google redirects the user’s browser to your redirect_uri with an authorization code appended to the URL. This code is short-lived — typically valid for about 60 seconds — and can only be used once.&lt;/p&gt;

&lt;p&gt;Notice what Google did not send: an access token. The code travels through the browser, which means it passes through browser history, HTTP logs, and referrer headers. Sending the actual token here would be a security hole. The code is intentionally useless on its own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4 — Your app exchanges the code for tokens.&lt;/strong&gt;&lt;br&gt;
This step happens entirely server-to-server. Your Spring Boot backend takes the code and calls Google’s token endpoint directly — not through the browser. It sends the code along with your client_secret. Google verifies both and issues the tokens.&lt;/p&gt;

&lt;p&gt;This server-to-server exchange is the entire security model of the Authorization Code Flow. The client_secret never touches the browser. The actual token never travels through a URL. The browser only ever saw a short-lived code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5 — Google returns an access token and an ID token.&lt;/strong&gt;&lt;br&gt;
The access_token is what you use to call Google's APIs on the user's behalf. The id_token is an OIDC JWT — it contains the user's identity: their Google ID, email, name, and profile picture. If you read Part 2 of this series, this JWT follows the same structure — header, payload, signature — that OncePerRequestFilter was validating.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6 — Your app reads the id token, loads the user, and populates the SecurityContext.&lt;/strong&gt;&lt;br&gt;
Spring Security decodes theid_token, extracts the user's details, and stores an authentication object in SecurityContextHolder. From here, the rest of your application sees a fully authenticated user — through @AuthenticationPrincipal, SecurityContextHolder.getContext(), or a principal in your controllers. The same as username-password login.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where OAuth2 plugs into Spring Security’s filter chain
&lt;/h2&gt;

&lt;p&gt;This is the part no other OAuth2 tutorial can show you — because they did not write Part 1 first.&lt;/p&gt;

&lt;p&gt;In Part 1, you traced how FilterChainProxy selects a SecurityFilterChain, how authentication filters extract credentials, how ProviderManager delegates to an AuthenticationProvider, and how everything ends up in SecurityContextHolder.&lt;/p&gt;

&lt;p&gt;OAuth2 does not replace that chain. It extends it with two new filters.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8tsdce4icolxllcorgvw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8tsdce4icolxllcorgvw.png" alt=" " width="800" height="729"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OAuth2AuthorizationRequestRedirectFilter&lt;/strong&gt; — this fires when a user hits /oauth2/authorization/google (or whichever provider you configured). It builds the full authorization URL — including generating and storing the state parameter — and redirects the user's browser to Google. This is what runs in Step 1 and 2 of the flow above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OAuth2LoginAuthenticationFilter&lt;/strong&gt; — this fires when Google redirects back to your /login/oauth2/code/google callback URL. It reads the authorization code from the request, verifies the state parameter, calls Google's token endpoint to exchange the code (Step 4), receives the tokens (Step 5), and creates an OAuth2LoginAuthenticationToken.&lt;/p&gt;

&lt;p&gt;That token then goes to ProviderManager — the same ProviderManager you already know from Part 1. ProviderManager loops through its registered providers, finds OidcAuthorizationCodeAuthenticationProvider, and calls it.&lt;/p&gt;

&lt;p&gt;OidcAuthorizationCodeAuthenticationProvider validates the id_token JWT signature, checks its expiry and claims, and calls OAuth2UserService to load or create your application's user. Then it returns a fully authenticated Authentication object.&lt;/p&gt;

&lt;p&gt;That object goes into SecurityContextHolder. Same destination as username-password login. Different path to get there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OAuth2UserService — where your app takes over&lt;/strong&gt;&lt;br&gt;
Spring Security handles everything up to this point automatically. OAuth2UserService is where your application steps in.&lt;/p&gt;

&lt;p&gt;After validating the id_token, Spring Security calls OAuth2UserService.loadUser() to turn Google's user attributes into something your application understands. The default implementation, DefaultOAuth2UserService, fetches the user's profile from Google's userinfo endpoint and returns an OAuth2User with those attributes.&lt;/p&gt;

&lt;p&gt;In most production systems, you need more than that. Google gives you a name and email. Your database has a User entity with an internal ID, roles, preferences, and account status. You override DefaultOAuth2UserService to bridge that gap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    @Autowired
    private UserRepository userRepository;
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(userRequest);
        String email = oAuth2User.getAttribute("email");
        // load or create the user in your own DB
        User user = userRepository.findByEmail(email)
            .orElseGet(() -&amp;gt; createNewUser(oAuth2User));
        return new DefaultOAuth2User(
            Collections.singleton(new SimpleGrantedAuthority(user.getRole())),
            oAuth2User.getAttributes(),
            "email"
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not an OAuth2 concept — it is a Spring Security hook. But it is where most “Login with Google” implementations actually live. The config wires up the flow. This class decides what your application does with the identity Google hands back.&lt;/p&gt;

&lt;h2&gt;
  
  
  The state parameter — the detail almost everyone skips
&lt;/h2&gt;

&lt;p&gt;Every authorization request your app sends to Google includes a state parameter — a randomly generated value that Spring Security creates and stores in the session. When Google redirects back to your app, it returns that same state value unchanged.&lt;/p&gt;

&lt;p&gt;Your app verifies that the returned state matches the one it stored before doing anything else.&lt;/p&gt;

&lt;p&gt;If you skip this check, an attacker can craft a malicious link that starts a login flow and redirects to your callback with their own authorization code. The victim clicks the link, your app completes the exchange, and the victim is now logged into the attacker’s account — with full access to whatever your app shows an authenticated user.&lt;/p&gt;

&lt;p&gt;This is a CSRF attack against OAuth2. Spring Security handles state generation and validation automatically. You do not need to write this logic. But if you ever build an OAuth2 flow manually — or review one built without a framework — missing state validation is the first thing to check.&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to use OAuth2
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do not use the Implicit Flow&lt;/strong&gt;. It was removed from OAuth2.1. It sent tokens directly in the URL, which means tokens in browser history, server logs, and referrer headers. There is no legitimate reason to use it today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not store access tokens in localStorage&lt;/strong&gt;. XSS vulnerabilities can read localStorage trivially. Use HttpOnly cookies, or keep tokens server-side in the session, where JavaScript cannot touch them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not use OAuth2 when you do not need it&lt;/strong&gt;. If your application has its own user database and no external identity provider, JWT authentication — which you traced in Part 2 — is simpler. You own the entire flow, you are not dependent on a third party’s uptime, and there is no redirect dance to debug. OAuth2 earns its complexity only when the third-party identity or the delegated access to third-party resources is genuinely valuable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not forget token expiry&lt;/strong&gt;. Access tokens are short-lived by design. If you store a token and reuse it without checking expiry, your users will get silent authentication failures at the worst possible moment. Build refresh token logic from the start, not as an afterthought.&lt;/p&gt;

</description>
      <category>springsecurity</category>
      <category>oauth</category>
      <category>springboot</category>
      <category>java</category>
    </item>
    <item>
      <title>https://dev.to/piyushsingh_dev/spring-ai-tool-calling-explained-how-to-give-your-llm-real-superpowers-3g04</title>
      <dc:creator>Piyush Kumar Singh</dc:creator>
      <pubDate>Sat, 06 Jun 2026 14:53:18 +0000</pubDate>
      <link>https://dev.to/piyushsingh_dev/-p7e</link>
      <guid>https://dev.to/piyushsingh_dev/-p7e</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/piyushsingh_dev/spring-ai-tool-calling-explained-how-to-give-your-llm-real-superpowers-3g04" class="crayons-story__hidden-navigation-link"&gt;Spring AI Tool Calling Explained | How to Give Your LLM Real Superpowers&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/piyushsingh_dev" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3909333%2Fe0081e0c-5341-42fe-b711-92dafd3e0fdb.jpg" alt="piyushsingh_dev profile" class="crayons-avatar__image" width="720" height="720"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/piyushsingh_dev" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Piyush Kumar Singh
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Piyush Kumar Singh
                
              
              &lt;div id="story-author-preview-content-3835519" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/piyushsingh_dev" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3909333%2Fe0081e0c-5341-42fe-b711-92dafd3e0fdb.jpg" class="crayons-avatar__image" alt="" width="720" height="720"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Piyush Kumar Singh&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/piyushsingh_dev/spring-ai-tool-calling-explained-how-to-give-your-llm-real-superpowers-3g04" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jun 6&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/piyushsingh_dev/spring-ai-tool-calling-explained-how-to-give-your-llm-real-superpowers-3g04" id="article-link-3835519"&gt;
          Spring AI Tool Calling Explained | How to Give Your LLM Real Superpowers
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/springboot"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;springboot&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/llm"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;llm&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/rag"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;rag&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/piyushsingh_dev/spring-ai-tool-calling-explained-how-to-give-your-llm-real-superpowers-3g04#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              1&lt;span class="hidden s:inline"&gt;&amp;nbsp;comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            6 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Spring AI Tool Calling Explained | How to Give Your LLM Real Superpowers</title>
      <dc:creator>Piyush Kumar Singh</dc:creator>
      <pubDate>Sat, 06 Jun 2026 14:51:01 +0000</pubDate>
      <link>https://dev.to/piyushsingh_dev/spring-ai-tool-calling-explained-how-to-give-your-llm-real-superpowers-3g04</link>
      <guid>https://dev.to/piyushsingh_dev/spring-ai-tool-calling-explained-how-to-give-your-llm-real-superpowers-3g04</guid>
      <description>&lt;p&gt;Ask ChatGPT what Apple’s stock price is right now. It’ll either tell you it doesn’t have real-time data or confidently give you a number that’s months old. That’s not a bug in the model. It’s a fundamental limitation of how LLMs work. They’re trained on data up to a certain date, sealed, and shipped. They can’t call your database. They can’t check the weather. They can’t look up a customer’s order status. They know nothing about the world after their training cutoff — and nothing about your world at all.&lt;/p&gt;

&lt;p&gt;Tool calling is how you fix that. And it’s one of the core building blocks of what people are calling AI agents — systems where an LLM can take action, not just generate text.&lt;/p&gt;

&lt;h2&gt;
  
  
  What tool calling actually is
&lt;/h2&gt;

&lt;p&gt;Before diving into Spring AI, let’s get the concept straight in plain language.&lt;/p&gt;

&lt;p&gt;Imagine you hire an extremely smart consultant. They know a lot — strategy, frameworks, history, writing. But they don’t have access to your company’s internal systems. They can’t log into your CRM and check whether a customer paid last month.&lt;/p&gt;

&lt;p&gt;So you make an arrangement: whenever they need internal data to answer a question, they hand you a sticky note saying, “I need order #4521’s status.” You go look it up. You bring them the answer. They use it to finish their response.&lt;/p&gt;

&lt;p&gt;That sticky note exchange is tool calling. You may also see it called function calling — same concept, different name depending on which LLM provider’s docs you’re reading.&lt;/p&gt;

&lt;p&gt;The LLM doesn’t execute code. It doesn’t call APIs. It doesn’t touch your database directly. It just says, “I need this specific piece of information from this specific function.” Your application handles the actual execution and returns the result. The LLM then uses that result to write its final response.&lt;/p&gt;

&lt;p&gt;That distinction — the model asks, your app executes — is the most important thing to understand about tool calling. And most tutorials skip right past it.&lt;br&gt;
The 5-step loop — what happens inside every tool call&lt;/p&gt;
&lt;h2&gt;
  
  
  Spring AI Tool call
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa23jdx9fnojyzx7507af.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa23jdx9fnojyzx7507af.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The diagram above shows the full cycle. Here’s each step in plain terms:&lt;/p&gt;

&lt;p&gt;Step 1 — User asks a question. “What is Apple’s stock price right now?”&lt;/p&gt;

&lt;p&gt;Step 2 — The LLM reads the question and your tool list. When you register tools with Spring AI, each one has a name and a description. The LLM reads those descriptions and makes a decision: can I answer this from my training data, or do I need to call a tool?&lt;/p&gt;

&lt;p&gt;Step 3 — The LLM returns a tool request, not an answer. This is the part people don’t expect. Instead of answering your question, the model sends back something like: “Call the getStockPrice function with argument AAPL.” Just a tool name and arguments. No answer yet.&lt;/p&gt;

&lt;p&gt;Step 4 — Your Spring app executes the method. Spring AI picks up the tool request, finds the matching @Tool method in your code, calls it, and gets the real data — from your API, your database, whatever you wired in.&lt;/p&gt;

&lt;p&gt;Step 5 — The result goes back to the LLM. Spring AI sends the function’s return value back into the conversation context. The LLM reads it and now writes its actual response: “Apple is trading at $213.45 as of this morning.”&lt;/p&gt;

&lt;p&gt;The user sees a real, grounded answer. No hallucination. No outdated training data. The model provided the intelligence, your application provided the data.&lt;/p&gt;
&lt;h2&gt;
  
  
  The @Tool annotation: registering tools in Spring AI
&lt;/h2&gt;

&lt;p&gt;In Spring AI, turning a regular Java method into a tool the LLM can call takes one annotation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StockPriceTool&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

&lt;span class="nd"&gt;@Tool&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Get the current stock price for a given ticker symbol"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;getStockPrice&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="nd"&gt;@ToolParam&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"The stock ticker symbol, e.g. AAPL, TSLA"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// call a real stock API here&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;financialApiClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCurrentPrice&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things here that most tutorials don’t explain:&lt;/p&gt;

&lt;p&gt;The description on @Tool is not documentation for you — it’s instructions for the LLM. The model reads this text to decide when to call your function. A vague description like “gets stock data” will confuse the model. A precise one like “Get the current stock price for a given ticker symbol” tells the model exactly when this tool applies. Treat it like a prompt, not a comment.&lt;/p&gt;

&lt;p&gt;The same goes for @ToolParam. The LLM reads your parameter descriptions to understand what values it should pass. If your description is unclear, the model will pass the wrong arguments. Bad input = bad output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registering your tools with ChatClient
&lt;/h2&gt;

&lt;p&gt;Once your tool class is ready, you wire it into ChatClient through your configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AiConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Autowired&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;StockPriceTool&lt;/span&gt; &lt;span class="n"&gt;stockPriceTool&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ChatClient&lt;/span&gt; &lt;span class="nf"&gt;chatClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultTools&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stockPriceTool&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spring AI scans the bean, finds every method annotated with @Tool, and registers them automatically. No verbose schema definitions, no manual type mappings — just your annotated method and a single line in your configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the LLM reads and why description quality is everything
&lt;/h2&gt;

&lt;p&gt;Think about what happens from the model’s perspective. Before answering any question, it sees something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Available tools:
- getStockPrice: Get the current stock price for a given ticker symbol
- getWeatherForecast: Get 3-day weather forecast for a city
- getUserOrderHistory: Get the last 10 orders for a given customer ID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That list of descriptions is the only information the model has about what your tools can do. It uses those descriptions to reason about which tool — if any — applies to the current question.&lt;/p&gt;

&lt;p&gt;If someone asks, “Is it going to rain in Mumbai tomorrow?” — the model reads the descriptions, matches them to getWeatherForecast, and calls it.&lt;/p&gt;

&lt;p&gt;If someone asks, “Should I carry an umbrella to the client meeting?” — the model has to infer that “client meeting” implies weather concern, decide Mumbai is a reasonable assumption if that’s the user’s location, and call the right tool accordingly.&lt;/p&gt;

&lt;p&gt;All of that reasoning happens against your description strings. Write them well, and the model becomes genuinely useful. Write them carelessly, and the model either calls the wrong tool, passes wrong arguments, or skips tools it should use.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the LLM chains tools and the security
&lt;/h2&gt;

&lt;p&gt;Once you register multiple tools, something interesting happens: the LLM can call them in sequence. It might call getCustomerProfile first, get the customer’s account tier, then call getOrderHistory with that tier to filter the results, then call calculateDiscount based on the history.&lt;/p&gt;

&lt;p&gt;This is the foundation of what people call AI agents — LLMs that don’t just answer questions but complete multi-step tasks by composing tools. Spring AI’s approach to this connects naturally with MCP (Model Context Protocol), a standard for exposing tools to LLMs in a consistent, discoverable way — but that’s a deeper topic for another article.&lt;/p&gt;

&lt;p&gt;Here’s the security consideration that almost every tutorial skips: the LLM cannot call your database directly. It cannot make HTTP requests. It cannot run arbitrary code. Every tool execution goes through your Spring code — which means every tool execution can be authenticated, rate-limited, logged, and validated exactly like any other function call in your system.&lt;/p&gt;

&lt;p&gt;That’s not a limitation. That’s the design. You can safely expose powerful capabilities to the LLM without handing over the keys to your infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to use the tool calling
&lt;/h2&gt;

&lt;p&gt;Don’t use a tool calling for questions the LLM can answer from its training. Every tool call adds a round trip — your app waits for the LLM to respond, executes the function, and sends a second request back. That’s real latency. If someone asks, “explain what a P/E ratio is”, there’s no need to call a financial API. Reserve tool calls for data the model genuinely can’t have.&lt;/p&gt;

&lt;p&gt;Watch out for circular tool chains. If Tool A can trigger Tool B, which can trigger Tool A again under certain conditions, you can end up in a loop. Spring AI doesn’t automatically break cycles. Define clear, narrow tool responsibilities to avoid this.&lt;/p&gt;

&lt;p&gt;Description drift is a real maintenance problem. When your underlying function changes — new parameters, changed logic, different return format — you need to update your @Tool description to match. The LLM reasons from the description, not the code. Stale descriptions lead to bad arguments and broken calls.&lt;/p&gt;

&lt;p&gt;Latency stacks with every hop. One tool call adds roughly 500ms–1s. Three tool calls in sequence can add 2–3 seconds. For user-facing chatbots, that’s noticeable. Design your tools to be composable but not deeply chained unless the use case genuinely requires it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spring AI 2.x — a note if you’re upgrading
&lt;/h2&gt;

&lt;p&gt;If you’ve followed a tool-calling tutorial written before mid-2025 and the code isn’t working, this is likely why.&lt;/p&gt;

&lt;p&gt;The old approach used FunctionCallback and verbose bean registration, long configuration chains, manual input/output type definitions, and FunctionCallbackWrapper setup. Spring AI has moved away from all of that.&lt;/p&gt;

&lt;p&gt;The old .tools(“toolName”) syntax no longer works reliably in Spring AI 1.0 GA. If your tools aren’t being picked up, check that you’re passing the bean directly to defaultTools(myToolBean), not a string name.&lt;/p&gt;

&lt;p&gt;The new annotation-based approach shown in this article is the current recommended path. It’s cleaner, requires no manual schema definitions, and fits naturally into how Spring Boot beans already work. If you’re migrating from an older version, the change is mostly mechanical: replace FunctionCallback registrations with @Tool annotated methods and update your ChatClient configuration.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>springboot</category>
      <category>llm</category>
      <category>rag</category>
    </item>
    <item>
      <title>Spring AI Explained — ChatClient, RAG, Advisors, and Every Core Component</title>
      <dc:creator>Piyush Kumar Singh</dc:creator>
      <pubDate>Mon, 18 May 2026 04:30:23 +0000</pubDate>
      <link>https://dev.to/piyushsingh_dev/spring-ai-explained-chatclient-rag-advisors-and-every-core-component-14dl</link>
      <guid>https://dev.to/piyushsingh_dev/spring-ai-explained-chatclient-rag-advisors-and-every-core-component-14dl</guid>
      <description>&lt;p&gt;Most Spring AI tutorials jump straight to code. You copy the dependency, paste the config, call ChatClient, and something works. But when you need to actually build something — a chatbot that remembers conversations, an API that answers questions from your own documents — you hit a wall. Because you don't know what's actually doing what. Friend’s Link&lt;/p&gt;

&lt;h2&gt;
  
  
  What Spring AI actually is — in one sentence
&lt;/h2&gt;

&lt;p&gt;Spring AI is an abstraction layer that lets you wire LLMs into your Spring Boot app without hardcoding any particular AI provider.&lt;/p&gt;

&lt;p&gt;That last part matters. OpenAI, Google Gemini, Anthropic Claude, and Ollama are running locally on your machine — Spring AI talks to all of them through the same API. Swap providers without touching your business logic. That’s the entire value proposition, and everything else is built on top of it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm7k5ulejjqctedmvvu7w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm7k5ulejjqctedmvvu7w.png" alt=" " width="680" height="580"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Spring AI Components
&lt;/h2&gt;

&lt;p&gt;ChatClient — the front door&lt;br&gt;
ChatClient is the component you'll interact with the most. It's the fluent API that sits at the top of the stack and handles the actual request-response cycle with the LLM.&lt;/p&gt;

&lt;p&gt;Think of it like a RestTemplate or a WebClient— but instead of calling a REST endpoint, you're sending a prompt and getting a response back. It handles all the low-level connection details, request formatting, and response parsing so you don't have to.&lt;/p&gt;

&lt;p&gt;What makes ChatClient genuinely well-designed is its fluent builder style. You don't configure it once globally and hope for the best. Each call is composable — you can set the system prompt, attach advisors, pass user input, and control the output format all in one readable chain.&lt;/p&gt;

&lt;p&gt;It also separates two things that often get conflated: the default configuration you set at startup (your system prompt, default advisors, model parameters) and the per-request configuration you apply at call time. That separation matters in production, where different endpoints need different behaviours from the same underlying client.&lt;/p&gt;

&lt;h2&gt;
  
  
  PromptTemplate — how you talk to the LLM properly
&lt;/h2&gt;

&lt;p&gt;A raw string shoved into an LLM is not a prompt. A prompt is a structured piece of text with placeholders, context, and instructions — and this PromptTemplate is how Spring AI handles that.&lt;/p&gt;

&lt;p&gt;The idea is simple: you define a template with variables, and at runtime, you fill those variables in. Instead of building prompt strings with Java string concatenation — which gets messy fast — you define the shape of the prompt separately from the data that goes into it.&lt;/p&gt;

&lt;p&gt;This matters for three reasons. First, it keeps prompts readable and maintainable. Second, it separates the “what to ask” from the “what data to inject” which is the same separation concerns you apply everywhere else in your codebase. Third, it makes prompt versioning possible. When your prompt needs tweaking, you’re editing a template, not hunting through business logic.&lt;/p&gt;

&lt;p&gt;PromptTemplate also gives you a proper Prompt object that carries both the human message and the system message. That distinction — system prompt (the instructions) vs user prompt (the question) — is one of the most important things to understand when working with LLMs, and Spring AI models it explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  EmbeddingModel — the piece that makes search smart
&lt;/h2&gt;

&lt;p&gt;An EmbeddingModel takes text and converts it into a vector — a list of floating point numbers that represents the meaning of that text in multi-dimensional space.&lt;/p&gt;

&lt;p&gt;That sounds abstract. Here’s the concrete thing to grasp: two pieces of text that mean similar things will produce vectors that are close to each other mathematically. “What’s your return policy?” and “How do I get a refund?” are different strings, but their vectors will be very close — because semantically, they’re the same question.&lt;/p&gt;

&lt;p&gt;This is what makes semantic search possible. Traditional search matches keywords. Embedding-based search matches meaning. A user asking “how do I cancel my order” will find a document titled “Order cancellation policy” even if the words don’t overlap, because the meanings are geometrically close in vector space.&lt;/p&gt;

&lt;p&gt;In Spring AI, EmbeddingModel is the interface that abstracts over whatever embedding service you're using — OpenAI's text-embedding-ada-002, Gemini's embedding API, or a local model via Ollama. The abstraction is consistent regardless of provider, which means your RAG pipeline doesn't break if you switch models.&lt;/p&gt;

&lt;h2&gt;
  
  
  VectorStore — where embeddings live
&lt;/h2&gt;

&lt;p&gt;VectorStore is the database for embeddings. You put vectors in, and you query them by similarity — "give me the top 5 stored vectors that are closest to this query vector."&lt;/p&gt;

&lt;p&gt;It’s worth understanding that this is a fundamentally different kind of database from what you’re used to. You don’t query it with SQL. You don’t look things up by ID. You ask: which stored content is most semantically similar to this input? And it returns the matches ranked by similarity score.&lt;/p&gt;

&lt;p&gt;Spring AI’s VectorStore interface abstracts over the actual storage engine underneath. In development, you might use SimpleVectorStore an in-memory implementation. In production, you'd swap to Pinecone, Weaviate, pgvector on top of Postgres, or Elasticsearch. The interface stays identical. Your code doesn't change.&lt;/p&gt;

&lt;p&gt;The VectorStore is also responsible for handling the metadata that travels alongside each vector — the document title, page number, source URL, whatever you stored at ingestion time. When it returns matching chunks, that metadata comes with it, so your prompt builder knows where the information came from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advisors — the middleware nobody talks about enough
&lt;/h2&gt;

&lt;p&gt;This is the component most tutorials skip, and it’s arguably the most powerful part of the whole framework.&lt;/p&gt;

&lt;p&gt;An Advisor in Spring AI is a piece of middleware that wraps around every ChatClient request. Before the request goes to the LLM, advisors can intercept it and modify it — add context, inject memory, apply safety rules, log the conversation, filter the input. After the response comes back, they can post-process it too.&lt;/p&gt;

&lt;p&gt;The important thing to understand is that advisors form a chain. Each one wraps the next, like servlet filters in a web application. You configure which advisors run in which order, and each one has a defined responsibility.&lt;/p&gt;

&lt;p&gt;QuestionAnswerAdvisor is the one you'll use for RAG. Before your question reaches the LLM, this advisor takes that question, queries VectorStore for the most relevant chunks, and injects them into the prompt automatically. From ChatClient's perspective, you just asked a question. Internally, your question has been enriched with your own data before the LLM ever sees it.&lt;/p&gt;

&lt;p&gt;MessageChatMemoryAdvisor is what makes conversations persistent. Without it, every call to ChatClient starts fresh — no memory of what was said before. With it, previous turns from ChatMemory are injected into each new request so the LLM has context.&lt;/p&gt;

&lt;p&gt;You can write your own advisors too. Any cross-cutting concern that applies to every LLM call — rate limiting, PII detection, response caching, A/B testing between prompts — belongs in an advisor, not in your business logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  ChatMemory — giving the LLM a memory
&lt;/h2&gt;

&lt;p&gt;LLMs are stateless. Every API call is completely independent. Ask an LLM “what’s the capital of France,” then ask “what did I just ask you,” and it has no idea — because, from its perspective, that second request is the first thing you’ve ever said.&lt;/p&gt;

&lt;p&gt;ChatMemory is how Spring AI solves this. It's a storage abstraction for conversation history. After each exchange, the message — both the user's question and the LLM's response — gets saved. On the next request, that history gets loaded and injected into the prompt so the LLM has context.&lt;/p&gt;

&lt;p&gt;InMemoryChatMemory is the default — history lives in your application's heap and disappears on restart. That's fine for development and short stateless sessions. For production chatbots that need to remember users across sessions, you'd implement a persistent ChatMemory backed by Redis or a database.&lt;/p&gt;

&lt;p&gt;There’s a real constraint here worth knowing upfront: every message you inject into the conversation history costs tokens. LLMs have a context window limit — usually somewhere between 8K and 128K tokens, depending on the model. If a conversation goes on long enough, the accumulated history will either exceed the limit and fail, or you’ll need to implement a summarisation strategy to compress older messages.&lt;/p&gt;

&lt;p&gt;This is not a Spring AI problem — it’s a fundamental LLM constraint. But ChatMemory is where you manage it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo7bqasro35b8lns9oo5w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo7bqasro35b8lns9oo5w.png" alt=" " width="696" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  RAG Flow
&lt;/h2&gt;

&lt;p&gt;How RAG brings it all together&lt;br&gt;
RAG — Retrieval-Augmented Generation — is the pattern that makes Spring AI genuinely production-useful. The diagram above shows both phases. Here’s the thinking behind it.&lt;/p&gt;

&lt;p&gt;The core problem: your LLM knows nothing about your company. It doesn’t know your product documentation, your internal policies, your customer data. Fine-tuning a model on your data is expensive, slow, and goes stale every time the data changes.&lt;/p&gt;

&lt;p&gt;RAG is the pragmatic answer. Instead of teaching the model your data, you just hand it the relevant pages at the moment it needs them. Like giving a contractor a specific clause from the contract rather than asking them to memorise the whole thing.&lt;/p&gt;

&lt;p&gt;The ingestion phase runs once, or whenever your data changes. Your documents are loaded, split into manageable chunks, embedded into vectors, and stored in a VectorStore. This is how your data gets indexed for semantic retrieval.&lt;/p&gt;

&lt;p&gt;The query phase runs on every request. The user’s question is embedded into a vector. That vector is used to query the VectorStore for the closest matching chunks. Those chunks — plus the original question — get injected into the prompt. The LLM reads them as context and answers based on what it finds there.&lt;/p&gt;

&lt;p&gt;The LLM never “learned” your data. It reads it fresh on each request, like an open-book exam. That framing matters because it sets the right expectations: if the relevant information isn’t in the retrieved chunks, the model will still try to answer — and that’s when hallucinations happen. RAG reduces hallucinations by providing grounding. It doesn’t eliminate them.&lt;/p&gt;

&lt;p&gt;The part that controls retrieval quality isn’t the LLM and isn’t the vector database — it’s the chunking strategy. How you split your documents determines what gets retrieved. A chunk that’s too large buries the relevant detail in noise. A chunk too small loses the surrounding context that makes it meaningful. Getting chunking right is usually where the real tuning work happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line mental model for each component
&lt;/h2&gt;

&lt;p&gt;ChatClient — you talk to the LLM through this. PromptTemplate — You structure what you say. EmbeddingModel — converts meaning into math. VectorStore — stores and searches that math. Advisors — middleware that enriches every request automatically. ChatMemory — gives the conversation a past. Together, they’re the full stack for building LLM features that actually behave like software — predictable, configurable, and debuggable.&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>https://dev.to/piyush_kumarsingh_da3833/how-redis-actually-works-ram-single-thread-and-the-expiry-behavior-nobody-explains-2j4n</title>
      <dc:creator>Piyush Kumar Singh</dc:creator>
      <pubDate>Sun, 03 May 2026 16:48:10 +0000</pubDate>
      <link>https://dev.to/piyushsingh_dev/-h21</link>
      <guid>https://dev.to/piyushsingh_dev/-h21</guid>
      <description>&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://dev.to/piyushsingh_dev/how-redis-actually-works-ram-single-thread-and-the-expiry-behavior-nobody-explains-2j4n" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F59plhuurbjomf48y12e1.png" height="533" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://dev.to/piyushsingh_dev/how-redis-actually-works-ram-single-thread-and-the-expiry-behavior-nobody-explains-2j4n" rel="noopener noreferrer" class="c-link"&gt;
            How Redis Actually Works — RAM, Single Thread, and the Expiry Behavior Nobody Explains - DEV Community
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            A RAM read takes about 100 nanoseconds. A disk read — even on a modern SSD — takes around 100,000...
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8j7kvp660rqzt99zui8e.png" width="300" height="299"&gt;
          dev.to
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
    </item>
    <item>
      <title>How Redis Actually Works — RAM, Single Thread, and the Expiry Behavior Nobody Explains</title>
      <dc:creator>Piyush Kumar Singh</dc:creator>
      <pubDate>Sun, 03 May 2026 16:45:01 +0000</pubDate>
      <link>https://dev.to/piyushsingh_dev/how-redis-actually-works-ram-single-thread-and-the-expiry-behavior-nobody-explains-2j4n</link>
      <guid>https://dev.to/piyushsingh_dev/how-redis-actually-works-ram-single-thread-and-the-expiry-behavior-nobody-explains-2j4n</guid>
      <description>&lt;p&gt;A RAM read takes about 100 nanoseconds. A disk read — even on a modern SSD — takes around 100,000 nanoseconds. That single gap explains most of Redis’s speed, before it does a single thing clever. Friend’s Link&lt;/p&gt;

&lt;p&gt;But RAM alone isn’t the full story. The other half is a design decision that looks like a limitation on paper — and turns out to be one of the smartest choices in the codebase. More on that in a moment. Here’s what’s actually happening inside Redis when your app talks to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why is Redis so fast?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe0kih88g61ntm1pqmq58.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe0kih88g61ntm1pqmq58.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first reason is obvious once you hear it: Redis keeps everything in RAM. Your PostgreSQL instance, however well-tuned, writes to disk. Redis doesn’t. Every key lives in memory, which is why a GET on a Redis key can return in under a millisecond even under load. There’s no disk seek, no page cache miss, no I/O wait. But here’s where most explanations stop — and they shouldn’t.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Single-threaded — and that’s the point&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Redis processes one command at a time—one thread. No parallelism, no concurrency. That sounds like a bottleneck. It’s actually a feature.&lt;/p&gt;

&lt;p&gt;In a multi-threaded system, shared state requires locks. Locks mean threads waiting on each other. Waiting introduces latency spikes that are hard to reproduce and harder to debug. Redis avoids the entire problem by never having two threads compete for the same data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# These three clients connect simultaneously
Client 1: SET counter 100     ← executes fully first
Client 2: INCR counter        ← executes next, sees 100, returns 101
Client 3: GET counter         ← executes last, returns 101
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order is deterministic. Always. You can reason about it. With threads and locks, you can’t—not without careful synchronization, which adds complexity and latency.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Redis is fast, not just because of RAM, but because it never waits on itself.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The six data structures — with the tradeoffs that actually matter&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Redis isn’t just strings. Each data structure solves a specific problem, and knowing when to pick one over another is more useful than knowing the commands.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;String&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SET user:1001:name "John"
GET user:1001:name        # "John"
SET page:views 0
INCR page:views           # atomic - safe under concurrent load
GET page:views            # "1"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default choice for simple values, flags, and counters. INCR is atomic — a thousand clients calling it simultaneously will never produce a wrong count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hash&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HSET user:1001 name "John" email "j@example.com" role "admin"
HGET user:1001 name       # "John"
HGETALL user:1001         # all fields
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Hash is better than a String when you have a structured object with multiple fields you’ll update independently. If you stored this as a JSON string, updating a single field means deserializing the whole blob, changing one value, and reserializing. A Hash lets you update one field with one command.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;List&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RPUSH notifications:1001 "Order shipped"
RPUSH notifications:1001 "Payment received"
LRANGE notifications:1001 0 -1   # all items in order
LPOP notifications:1001           # "Order shipped"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lists maintain insertion order. Use them for notification feeds, activity timelines, and simple job queues where you’re okay with at-most-once delivery. If you need guaranteed delivery, a List isn’t enough — use Kafka or RabbitMQ.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sorted Set&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ZADD leaderboard 9500 "alice"
ZADD leaderboard 11200 "John"
ZREVRANGE leaderboard 0 2 WITHSCORES
# John    11200
# alice   9500
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every member has a score. Redis keeps them sorted automatically. Real-time leaderboards, priority queues, and rate limiting windows — sorted sets handle all three. The reason to reach for this over a List is when rank or score matters, not just insertion order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SADD online:users "user:1001" "user:1002"
SISMEMBER online:users "user:1002"   # 1 (true)
SCARD online:users                   # 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No duplicates, O(1) membership check. Good for tracking online users, visited pages, or anything where “is X in this group” is the question. Use a Set over a List when you need uniqueness and don’t care about order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HyperLogLog&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PFADD page:views:home "ip1" "ip2" "ip3" "ip1"
PFCOUNT page:views:home   # 3 (deduplicated)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HyperLogLog gives you approximate unique counts using a fixed 12KB of memory — regardless of whether you have 1,000 or 100 million unique values. A plain Set would work too, but each unique member consumes memory. For a site with 50 million daily visitors, the Set version could eat gigabytes. HyperLogLog stays at 12KB with a ~0.81% error margin. That tradeoff is almost always worth it for analytics.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ferwpwba7a76prrnfvyu8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ferwpwba7a76prrnfvyu8.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;**&lt;/p&gt;

&lt;h2&gt;
  
  
  TTL and the expiry behavior nobody explains
&lt;/h2&gt;

&lt;p&gt;**&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SET session:abc123 "user_data" EX 3600   # expires in 1 hour
TTL session:abc123                        # 3597
# one hour later
GET session:abc123                        # (nil)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most developers assume Redis runs a background job that scans for expired keys and deletes them at the exact moment of expiry. It doesn’t. That would be expensive — imagine scanning millions of keys every second. Instead, Redis uses two strategies in parallel:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lazy deletion&lt;/strong&gt;: When you read a key, Redis checks its expiry first. If it’s expired, Redis deletes it right then and returns nil. Memory is reclaimed at access time, not expiry time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Active sampling&lt;/strong&gt;: Every 100ms, Redis randomly picks 20 keys that have TTLs set. If more than 25% of them are expired, it runs the loop again immediately. It keeps looping until the expired ratio drops below 25%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The consequence&lt;/strong&gt;: if you have 10 million keys expiring at 3 am and nothing reads them, the active sampler will gradually clean them up over the following minutes. Your memory won’t drop instantly. If you’re sizing Redis memory around key expiry, that lag is real, and you need to account for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Persistence — what survives a restart
&lt;/h2&gt;

&lt;p&gt;Redis lives in RAM. Restart the process, lose everything — unless you’ve configured persistence.&lt;/p&gt;

&lt;h2&gt;
  
  
  RDB snapshots
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# redis.conf
&lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt; &lt;span class="m"&gt;900&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;       &lt;span class="c"&gt;# snapshot if 1+ keys changed in 15 minutes
&lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;      &lt;span class="c"&gt;# snapshot if 10+ keys changed in 5 minutes
&lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt; &lt;span class="m"&gt;10000&lt;/span&gt;    &lt;span class="c"&gt;# snapshot if 10000+ keys changed in 1 minute
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Redis forks a child process and writes everything to dump.rdb. Fast to recover from. The risk: if your server crashes between snapshots, you lose whatever happened in that window. Fine for cache. Not fine for anything where losing recent writes matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  AOF — Append Only File
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# redis.conf
&lt;/span&gt;&lt;span class="n"&gt;appendonly&lt;/span&gt; &lt;span class="n"&gt;yes&lt;/span&gt;
&lt;span class="n"&gt;appendfsync&lt;/span&gt; &lt;span class="n"&gt;everysec&lt;/span&gt;   &lt;span class="c"&gt;# flush to disk every second
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every write command gets appended to a log. On restart, Redis replays the log. With every second, you lose at most one second of data. With always, you lose nothing, but your write throughput drops noticeably.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production setup — use both
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;save&lt;/span&gt; &lt;span class="m"&gt;900&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;appendonly&lt;/span&gt; &lt;span class="n"&gt;yes&lt;/span&gt;
&lt;span class="n"&gt;appendfsync&lt;/span&gt; &lt;span class="n"&gt;everysec&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RDB handles fast restarts. AOF handles durability. Together, they cover both failure modes without adding much overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Spring Boot integration — with the why behind the config&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Dependencies and connection&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- pom.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.boot&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-boot-starter-data-redis&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
# application.yml
spring:
  redis:
    host: localhost
    port: 6379
    timeout: 2000ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;RedisTemplate — why you need a custom serializer&lt;/strong&gt;&lt;br&gt;
By default, Spring uses Java serialization for values. That works, but it stores class names alongside data, making keys unreadable and tying you to your class structure. Switch to JSON serialization so your data is readable outside Spring too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;RedisTemplate&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;redisTemplate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RedisConnectionFactory&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;RedisTemplate&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RedisTemplate&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;
    &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setConnectionFactory&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setKeySerializer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StringRedisSerializer&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="c1"&gt;// Jackson JSON instead of Java serialization&lt;/span&gt;
    &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setValueSerializer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GenericJackson2JsonRedisSerializer&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nd"&gt;@Cacheable&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;two&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="n"&gt;layer&lt;/span&gt;
&lt;span class="nc"&gt;Spring&lt;/span&gt;&lt;span class="err"&gt;’&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;caching&lt;/span&gt; &lt;span class="n"&gt;abstraction&lt;/span&gt; &lt;span class="n"&gt;lets&lt;/span&gt; &lt;span class="n"&gt;you&lt;/span&gt; &lt;span class="n"&gt;add&lt;/span&gt; &lt;span class="nc"&gt;Redis&lt;/span&gt; &lt;span class="n"&gt;caching&lt;/span&gt; &lt;span class="n"&gt;without&lt;/span&gt; &lt;span class="n"&gt;touching&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt; &lt;span class="n"&gt;logic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="nc"&gt;The&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;every&lt;/span&gt; &lt;span class="n"&gt;subsequent&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;same&lt;/span&gt; &lt;span class="no"&gt;ID&lt;/span&gt; &lt;span class="n"&gt;returns&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="nl"&gt;Redis:&lt;/span&gt;

&lt;span class="nd"&gt;@Cacheable&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"users"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"#id"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="nf"&gt;getUserById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RuntimeException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"User not found"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@CacheEvict&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"users"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"#user.id"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="nf"&gt;updateUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// evicts stale cache on update&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nd"&gt;@SpringBootApplication&lt;/span&gt;
&lt;span class="nd"&gt;@EnableCaching&lt;/span&gt;   &lt;span class="c1"&gt;// don't forget this&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Application&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rate limiting with sorted sets&lt;/strong&gt;&lt;br&gt;
A sliding window rate limiter is one of Redis’s cleanest use cases. The sorted set score is the timestamp — so you can count requests within a time window with a range query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;isAllowed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;maxRequests&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;windowSeconds&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ratelimit:"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentTimeMillis&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;windowStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;windowSeconds&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="nc"&gt;ZSetOperations&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ops&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redisTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;opsForZSet&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;ops&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;removeRangeByScore&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;windowStart&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// drop old requests&lt;/span&gt;
    &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ops&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;zCard&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;maxRequests&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;ops&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;valueOf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;redisTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;expire&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;windowSeconds&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TimeUnit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SECONDS&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;**&lt;/p&gt;

&lt;h2&gt;
  
  
  When your tech lead says “add Redis,” — ask this first
&lt;/h2&gt;

&lt;p&gt;**&lt;br&gt;
There’s a version of this story everyone knows: the tech lead says “add Redis,” you add Redis, and something gets faster. Nobody questions it. But Redis has real constraints, and using it wrong is a common way to create problems that look like infrastructure issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don’t use it as your primary database&lt;/strong&gt;. Redis has no foreign keys, no joins, no complex queries. If your data has relationships, use a relational database. Redis is the layer on top, not the foundation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don’t store large values&lt;/strong&gt;. Redis works well with small, hot data. A 5MB JSON blob in Redis is possible and wasteful — you’re burning expensive RAM, hurting the event loop for every other client, and making serialization your bottleneck.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don’t use pub/sub for anything you can’t afford to lose&lt;/strong&gt;. Redis pub/sub has no persistence. If a subscriber goes offline for 30 seconds, those messages are gone. Use Kafka or RabbitMQ when reliability matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set a memory limit and eviction policy&lt;/strong&gt; — always. Without it, Redis will reject writes when it runs out of memory, and that failure mode is jarring in production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# redis.conf
maxmemory 2gb
maxmemory-policy allkeys-lru   # evict least recently used keys when full
**

## ```


The one line that ties it all together
**
Redis is fast because it stays in RAM and never waits for itself. Use it for caching, sessions, leaderboards, rate limiting, and lightweight pub/sub, where dropped messages are acceptable. Don’t ask it to be your source of truth. Understand those two constraints, and Redis stops being magic — it becomes a predictable tool that does exactly what you’d expect. That’s a good thing.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>redis</category>
      <category>softwareengineering</category>
      <category>database</category>
      <category>backend</category>
    </item>
    <item>
      <title>How Spring Security Works Internally (Filters, Authentication &amp; Authorization Explained)</title>
      <dc:creator>Piyush Kumar Singh</dc:creator>
      <pubDate>Sat, 02 May 2026 16:57:49 +0000</pubDate>
      <link>https://dev.to/piyushsingh_dev/how-spring-security-works-internally-filters-authentication-authorization-explained-2686</link>
      <guid>https://dev.to/piyushsingh_dev/how-spring-security-works-internally-filters-authentication-authorization-explained-2686</guid>
      <description>&lt;p&gt;If you have worked with Spring Boot for a while, you have used Spring Security without fully tracing what happens inside it.&lt;/p&gt;

&lt;p&gt;You add a dependency, configure a SecurityFilterChain, and wire a UserDetailsService, and your APIs are suddenly protected. It works. But under the hood, there is a very disciplined flow that decides who the user is, whether the password is valid, and whether the request should even reach your controller.&lt;/p&gt;

&lt;p&gt;Once that internal flow clicks, Spring Security stops feeling magical and starts feeling predictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Big Picture&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Every incoming HTTP request does not go straight to your controller. Before that request reaches DispatcherServlet, it passes through the servlet filter chain. Spring Security plugs itself into that chain and intercepts the request early.&lt;/p&gt;

&lt;p&gt;That matters because security decisions should happen before business logic runs. The flow looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0kvvss3j3my60eq7hips.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0kvvss3j3my60eq7hips.webp" alt=" " width="800" height="767"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is the full security journey in one line: intercept, authenticate, authorize, then continue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: The Request Enters the Servlet Filter Chain&lt;/strong&gt;&lt;br&gt;
When a client sends a request, Tomcat receives it first. Tomcat then passes it through a chain of servlet filters.&lt;/p&gt;

&lt;p&gt;These filters are not specific to Spring Security. They are part of the servlet infrastructure. Any framework can register filters here. Spring Security registers one important filter called FilterChainProxy.&lt;/p&gt;

&lt;p&gt;You can think of FilterChainProxy as the front desk for all Spring Security logic. It does not do all the security work itself. Instead, it decides which internal security filters should handle the request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: FilterChainProxy Picks the Right SecurityFilterChain&lt;/strong&gt;&lt;br&gt;
This is a key part that many developers miss. Spring Security does not always use one universal chain for every request. It can maintain multiple SecurityFilterChain configurations, each tied to different URL patterns or request matchers.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;/api/** may use JWT authentication&lt;br&gt;
/admin/** may require stricter role checks&lt;br&gt;
/login may use a form login&lt;br&gt;
FilterChainProxy checks the request and selects the correct chain using RequestMatcher. That means Spring Security is not just a collection of filters. It is a smart router for security filters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Authentication Filter Extracts Credentials&lt;/strong&gt;&lt;br&gt;
Once the correct security chain is selected, one of the authentication filters takes over. In the classic username-password login flow, that filter is usually UsernamePasswordAuthenticationFilter.&lt;/p&gt;

&lt;p&gt;Its job is simple:&lt;/p&gt;

&lt;p&gt;Read the username and password from the request&lt;br&gt;
Create an unauthenticated Authentication object&lt;br&gt;
Pass that object to AuthenticationManager&lt;br&gt;
At this point, the user is not yet trusted. The filter has only collected credentials. Verification still has to happen. This distinction is important. Extracting credentials and validating credentials are two separate responsibilities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: AuthenticationManager Coordinates Authentication&lt;/strong&gt;&lt;br&gt;
AuthenticationManager is the entry point for authentication logic. In most applications, the default implementation is ProviderManager.&lt;/p&gt;

&lt;p&gt;ProviderManager does not usually authenticate the user directly. Instead, it delegates to one of the configured AuthenticationProvider implementations. That design makes Spring Security flexible. Different providers can handle different authentication mechanisms:&lt;/p&gt;

&lt;p&gt;username and password&lt;br&gt;
JWT token&lt;br&gt;
OAuth2 login&lt;br&gt;
LDAP&lt;br&gt;
custom authentication rules&lt;br&gt;
When ProviderManager receives an authentication object, it loops through the registered providers and calls supports() on each one.&lt;/p&gt;

&lt;p&gt;The first provider that says, “Yes, I know how to handle this type of authentication,” gets the job. Then authenticate() is called on that provider.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: AuthenticationProvider Verifies the User&lt;/strong&gt;&lt;br&gt;
This is where the real authentication work happens. For username-password login, the provider is often DaoAuthenticationProvider.&lt;/p&gt;

&lt;p&gt;Its job usually includes two things:&lt;/p&gt;

&lt;p&gt;Load the user from a data source&lt;br&gt;
Validate the submitted password&lt;br&gt;
To load the user, it calls UserDetailsService.&lt;/p&gt;

&lt;p&gt;To validate the password, it uses PasswordEncoder.&lt;/p&gt;

&lt;p&gt;This split is one of the reasons Spring Security is so clean internally. Fetching user data and checking password hashing are handled by dedicated components, not mixed into one giant class.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6: UserDetailsService Loads the User From the Database&lt;/strong&gt;&lt;br&gt;
UserDetailsService is a very small but important contract.&lt;/p&gt;

&lt;p&gt;Its core method is:&lt;/p&gt;

&lt;p&gt;loadUserByUsername(String username)&lt;br&gt;
This method is responsible for fetching the user from your database, external system, or custom source. It returns a UserDetails object that contains:&lt;/p&gt;

&lt;p&gt;username&lt;br&gt;
password&lt;br&gt;
roles or authorities&lt;br&gt;
account status flags, such as locked or disabled&lt;br&gt;
If the user is not found, Spring Security throws an exception, and authentication fails. At this stage, the system now knows what the stored user record looks like. Next, it needs to compare the password.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 7: PasswordEncoder Validates the Password&lt;/strong&gt;&lt;br&gt;
Spring Security does not compare raw passwords directly. Instead, it uses a PasswordEncoder such as BCryptPasswordEncoder.&lt;/p&gt;

&lt;p&gt;Here is the idea:&lt;/p&gt;

&lt;p&gt;The password submitted by the client is plain text&lt;br&gt;
The stored password in the database is hashed&lt;br&gt;
PasswordEncoder.matches(rawPassword, encodedPassword) checks if they match safely&lt;br&gt;
If the password is wrong, authentication fails.&lt;/p&gt;

&lt;p&gt;If it matches, Spring Security creates a fully authenticated Authentication object containing the user’s identity and authorities. That object is now trusted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 8: SecurityContextHolder Stores the Authenticated User&lt;/strong&gt;&lt;br&gt;
Once authentication succeeds, Spring Security stores the authenticated user in SecurityContextHolder.&lt;/p&gt;

&lt;p&gt;This is what makes the user available for the rest of the request lifecycle.&lt;/p&gt;

&lt;p&gt;From here, other parts of the application can access the logged-in user through:&lt;/p&gt;

&lt;p&gt;SecurityContextHolder.getContext().getAuthentication()&lt;br&gt;
@AuthenticationPrincipal&lt;br&gt;
Principal in controllers&lt;br&gt;
In a regular servlet application, this security context is usually stored per request thread. That is why the controller can later know who the current user is without manually passing user details around.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 9: Authorization Happens Before the Controller&lt;/strong&gt;&lt;br&gt;
Authentication answers this question: Who are you?&lt;/p&gt;

&lt;p&gt;Authorization answers this one: What are you allowed to do?&lt;/p&gt;

&lt;p&gt;After the user is authenticated, Spring Security moves to authorization filters such as AuthorizationFilter.&lt;/p&gt;

&lt;p&gt;This stage checks whether the current user has the required role, authority, or permission for the requested resource.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;p&gt;hasRole(“ADMIN”)&lt;br&gt;
hasAuthority(“PAYMENT_READ”)&lt;br&gt;
Request matchers that restrict endpoints&lt;br&gt;
If authorization fails, Spring Security stops the request and returns an error such as 403 Forbidden. If authorization succeeds, the request continues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 10: The Request Reaches DispatcherServlet and Then the Controller&lt;/strong&gt;&lt;br&gt;
Only after authentication and authorization are complete does the request proceed to Spring MVC. Now, DispatcherServlet can route the request to the correct controller.&lt;/p&gt;

&lt;p&gt;At this point, your controller can safely assume one of two things:&lt;/p&gt;

&lt;p&gt;The endpoint is public, or&lt;br&gt;
The user has already been authenticated and authorized&lt;br&gt;
That separation is why controllers stay cleaner. Security is handled earlier in the pipeline instead of being scattered across business logic.&lt;/p&gt;

&lt;p&gt;What Happens If Authentication Fails?&lt;br&gt;
If authentication fails anywhere in the chain, Spring Security throws an authentication-related exception.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common outcomes include:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;401 Unauthorized for unauthenticated access&lt;br&gt;
Redirect to the login page in form-based login&lt;br&gt;
custom error response in REST APIs&lt;br&gt;
The controller is never called.&lt;/p&gt;

&lt;p&gt;This is a useful mental model: failed authentication stops the request before business logic begins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Makes Spring Security Feel Complex?&lt;/strong&gt;&lt;br&gt;
Usually, it is not the concepts. It is the number of moving parts.&lt;/p&gt;

&lt;p&gt;There are many classes involved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FilterChainProxy&lt;/li&gt;
&lt;li&gt;SecurityFilterChain&lt;/li&gt;
&lt;li&gt;UsernamePasswordAuthenticationFilter&lt;/li&gt;
&lt;li&gt;AuthenticationManager&lt;/li&gt;
&lt;li&gt;ProviderManager&lt;/li&gt;
&lt;li&gt;AuthenticationProvider&lt;/li&gt;
&lt;li&gt;UserDetailsService&lt;/li&gt;
&lt;li&gt;PasswordEncoder&lt;/li&gt;
&lt;li&gt;SecurityContextHolder&lt;/li&gt;
&lt;li&gt;AuthorizationFilter
At first glance, that looks like a lot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But if you group them by responsibility, it becomes manageable:&lt;/p&gt;

&lt;p&gt;_Filters _handle request interception&lt;br&gt;
_Manager _and providers handle authentication delegation&lt;br&gt;
_UserDetailsService _and PasswordEncoder validate identity&lt;br&gt;
_SecurityContextHolder _stores the authenticated user&lt;br&gt;
_Authorization _filters enforce access rules&lt;br&gt;
That is really the whole story.&lt;/p&gt;

&lt;p&gt;A Simple Way to Remember the Flow&lt;/p&gt;

&lt;p&gt;Use this line:&lt;/p&gt;

&lt;p&gt;Request comes in -&amp;gt; filter intercepts -&amp;gt; credentials extracted -&amp;gt; manager delegates -&amp;gt; provider authenticates -&amp;gt; context stores user -&amp;gt; authorization checks access -&amp;gt; controller runs&lt;/p&gt;

&lt;p&gt;If you remember that sentence, you already understand the internals better than many developers who use Spring Security every day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Takeaway
&lt;/h2&gt;

&lt;p&gt;Spring Security works like a layered checkpoint system. It intercepts the request before your application code sees it, verifies identity using providers and encoders, stores the authenticated user in a security context, checks permissions, and only then allows the request to hit the controller. Once you understand that flow, the framework feels a lot less intimidating.&lt;/p&gt;

</description>
      <category>java</category>
      <category>spring</category>
      <category>springsecurity</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
