<?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: srinivas reddy gouru</title>
    <description>The latest articles on DEV Community by srinivas reddy gouru (@srinivas_gouru_d26dc31f21).</description>
    <link>https://dev.to/srinivas_gouru_d26dc31f21</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3952982%2F742e0569-8b20-4ec8-8f6c-acb226355f7f.png</url>
      <title>DEV Community: srinivas reddy gouru</title>
      <link>https://dev.to/srinivas_gouru_d26dc31f21</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/srinivas_gouru_d26dc31f21"/>
    <language>en</language>
    <item>
      <title>How Spring Security works with JWT, OAuth2, and SSO</title>
      <dc:creator>srinivas reddy gouru</dc:creator>
      <pubDate>Tue, 26 May 2026 17:43:05 +0000</pubDate>
      <link>https://dev.to/srinivas_gouru_d26dc31f21/how-spring-security-works-with-jwt-oauth2-and-sso-5fil</link>
      <guid>https://dev.to/srinivas_gouru_d26dc31f21/how-spring-security-works-with-jwt-oauth2-and-sso-5fil</guid>
      <description>&lt;h2&gt;
  
  
  The 401 You Never Wrote: What Spring Security Actually Does Out of the Box
&lt;/h2&gt;

&lt;p&gt;Spring Security is a framework that intercepts every HTTP request entering your application and enforces authentication and authorisation rules before that request ever reaches a controller. You do not write the interception logic yourself; adding the &lt;code&gt;spring-boot-starter-security&lt;/code&gt; dependency to a Spring Boot project is enough to activate a default configuration that locks down every endpoint and returns a 401 (or redirects to &lt;code&gt;/login&lt;/code&gt;) for any unauthenticated caller. No annotations, no filters, no &lt;code&gt;@PreAuthorize&lt;/code&gt;, just a single dependency, and suddenly your previously open API is asking for credentials.&lt;/p&gt;

&lt;p&gt;That out-of-the-box behaviour surprises a lot of engineers the first time they see it, but it reflects a deliberate design choice: security should be the default, not something you opt into. The alternative, checking credentials inside each controller method, means one forgotten check equals one unprotected route. Spring Security removes that category of mistake entirely by applying rules uniformly at the HTTP layer, before a single line of your business logic runs. &lt;/p&gt;

&lt;p&gt;What makes this possible is the &lt;strong&gt;security filter chain&lt;/strong&gt;. Spring Security is not a single interceptor sitting in front of your application; it is a carefully ordered sequence of servlet filters, each responsible for one concern. One filter handles CSRF token validation. Another manages session creation and lookup. Another reads the &lt;code&gt;Authorisation&lt;/code&gt; header and tries to resolve a credential. Another checks whether the resolved identity actually has permission to reach the requested URL. These filters run in a fixed order on every incoming request, and each one can either pass the request downstream or short-circuit the chain with an error response.&lt;/p&gt;

&lt;p&gt;At the centre of this pipeline sits the &lt;code&gt;SecurityContext&lt;/code&gt;. By the time the authorisation filters run, the chain expects someone to have placed an &lt;code&gt;Authentication&lt;/code&gt; object into the context, a wrapper that holds the verified identity (the &lt;em&gt;principal&lt;/em&gt;) and the roles or authorities it carries. If the context is empty when the authorisation filter runs, the chain treats the caller as anonymous. If the requested URL requires an authenticated user, the request is rejected before your controller is ever invoked.&lt;/p&gt;

&lt;p&gt;This is the mental model that makes all of Spring Security coherent: a pipeline of filters, a shared context object, and one job delegated to whichever filter is responsible for your chosen credential mechanism. Every token mechanism you'll configure, JWT, OAuth2, SSO, is simply a different strategy for putting that principal into the context. Get that strategy wrong, or register it at the wrong point in the chain, and you'll keep seeing 401s that your controllers never threw.&lt;/p&gt;

&lt;h2&gt;
  
  
  The FilterChain: Spring Security's Request Processing Pipeline
&lt;/h2&gt;

&lt;p&gt;Every HTTP request that reaches a Spring application passes through a structure called the &lt;code&gt;SecurityFilterChain&lt;/code&gt; before it ever touches a controller. Understanding this chain is what separates confident Spring Security configuration from trial-and-error annotation hunting.&lt;/p&gt;

&lt;p&gt;Spring Security enters the picture through a &lt;code&gt;DelegatingFilterProxy&lt;/code&gt;, a thin Servlet filter that the framework registers with the container at startup. [1] Its only job is to hand the request off to Spring's application context, where the real work happens inside a &lt;code&gt;SecurityFilterChain&lt;/code&gt;: an ordered list of Spring-managed filters, each responsible for one specific concern. [1] [2] Think of it as a pipeline where each stage either passes the request forward, modifies it, or short-circuits the whole chain by writing a response directly, returning a &lt;code&gt;401&lt;/code&gt;, for example, before the request ever reaches your business logic.&lt;/p&gt;

&lt;p&gt;The order of filters in that chain is not arbitrary. Spring Security ships with defaults that place CSRF protection early, session management in the middle, and the authorisation check (&lt;code&gt;AuthorizationFilter&lt;/code&gt;) near the end. Authentication filters, wherever you insert them, must run before that authorisation check, because authorisation reads from something the authentication filter writes: the &lt;code&gt;SecurityContextHolder&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;SecurityContextHolder&lt;/code&gt; is a thread-local container that holds the &lt;code&gt;SecurityContext&lt;/code&gt;, and the &lt;code&gt;SecurityContext&lt;/code&gt; holds the &lt;code&gt;Authentication&lt;/code&gt; object for the current request. [1] [1] When an authentication filter validates a credential and calls &lt;code&gt;SecurityContextHolder.getContext().setAuthentication(...)&lt;/code&gt;, it is essentially leaving a note for every filter that comes after it: "this request belongs to a principal with these roles." The authorisation filter at the end of the chain reads that note and decides whether the principal's roles satisfy the rules you configured for that endpoint. If no authentication filter populates the context, the authorisation filter finds an anonymous token and rejects the request accordingly.&lt;/p&gt;

&lt;p&gt;This design has a practical consequence for configuration. When you call &lt;code&gt;http.addFilterBefore(myFilter, UsernamePasswordAuthenticationFilter.class)&lt;/code&gt; or &lt;code&gt;addFilterAfter&lt;/code&gt;, you are specifying exactly where in this ordered pipeline your logic runs. [2] Getting that position wrong is one of the most common sources of mysterious &lt;code&gt;403&lt;/code&gt; responses, because a filter that runs after the authorisation check can no longer influence the outcome of that check.&lt;/p&gt;

&lt;p&gt;Each of the three token mechanisms covered in the next sections plugs into this chain at a different point and populates the &lt;code&gt;SecurityContextHolder&lt;/code&gt; in a different way. A JWT filter reads the &lt;code&gt;Authorisation&lt;/code&gt; header, validates the token cryptographically, and sets the authentication directly, all within a single filter. OAuth2's resource server support does something similar but delegates signature verification to a remote or locally cached JWK set. SSO flows are different again: they redirect the browser entirely, establish a session on return, and then rely on a session-reading filter to restore the &lt;code&gt;SecurityContext&lt;/code&gt; on subsequent requests. Same chain, same &lt;code&gt;SecurityContextHolder&lt;/code&gt; contract, three different plug-in points. That consistency is what makes Spring Security composable once you internalise the pipeline model.&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%2Fy40c7oaucc42r3fn2vqr.jpg" 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%2Fy40c7oaucc42r3fn2vqr.jpg" alt=" " width="800" height="1417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  JWT Authentication: Stateless Token Validation in the Filter Chain
&lt;/h2&gt;

&lt;p&gt;With the filter chain's structure in mind, it's worth seeing exactly how a real token mechanism plugs into it. JWT is the most common starting point, partly because it requires no session storage and no round-trip to an authorisation server; the token itself carries everything the application needs to make an access decision.&lt;/p&gt;

&lt;p&gt;When a client sends a request to a secured endpoint, it includes a bearer token in the &lt;code&gt;Authorisation&lt;/code&gt; header. Spring Security doesn't know what to do with that header on its own; nothing in the default chain extracts a JWT. That gap is your extension point. You create a filter that extends &lt;code&gt;OncePerRequestFilter&lt;/code&gt;, extract the token from the header, validate its signature and expiry, and then construct an &lt;code&gt;Authentication&lt;/code&gt; object that you write into the &lt;code&gt;SecurityContextHolder&lt;/code&gt;. [1] From that moment forward in the chain, the request looks just like any other authenticated request; downstream filters and your controllers see a populated security context and never need to know a JWT was involved.&lt;/p&gt;

&lt;p&gt;Position matters here. Your custom filter must be registered &lt;em&gt;before&lt;/em&gt; &lt;code&gt;UsernamePasswordAuthenticationFilter&lt;/code&gt; in the chain, which you do with &lt;code&gt;addFilterBefore&lt;/code&gt; in your &lt;code&gt;SecurityFilterChain&lt;/code&gt; configuration. If it runs after the authorisation filters have already evaluated the request, the &lt;code&gt;SecurityContext&lt;/code&gt; will be empty at the moment it counts, and the request will be rejected as unauthenticated regardless of whether the token was valid.&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="nd"&gt;@RequiredArgsConstructor&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;JwtAuthenticationFilter&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;OncePerRequestFilter&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

 &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;JwtUtility&lt;/span&gt; &lt;span class="n"&gt;jwtUtility&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
 &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;UserDetailsService&lt;/span&gt; &lt;span class="n"&gt;userDetailsService&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

 &lt;span class="nd"&gt;@Override&lt;/span&gt;
 &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;doFilterInternal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpServletRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
 &lt;span class="nc"&gt;HttpServletResponse&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
 &lt;span class="nc"&gt;FilterChain&lt;/span&gt; &lt;span class="n"&gt;filterChain&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
 &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;ServletException&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;IOException&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;authHeader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getHeader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpHeaders&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AUTHORIZATION&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;authHeader&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;||!&lt;/span&gt;&lt;span class="n"&gt;authHeader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;startsWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Bearer "&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="n"&gt;filterChain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;doFilter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
 &lt;span class="k"&gt;return&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;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;authHeader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;substring&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&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;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwtUtility&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;extractUsername&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&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;username&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="nc"&gt;SecurityContextHolder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContext&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getAuthentication&lt;/span&gt;&lt;span class="o"&gt;()&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;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="nc"&gt;UserDetails&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userDetailsService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadUserByUsername&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&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;jwtUtility&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isTokenValid&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&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="o"&gt;{&lt;/span&gt;
 &lt;span class="nc"&gt;UsernamePasswordAuthenticationToken&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
 &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UsernamePasswordAuthenticationToken&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="kc"&gt;null&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="na"&gt;getAuthorities&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
 &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setDetails&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;WebAuthenticationDetailsSource&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;buildDetails&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
 &lt;span class="nc"&gt;SecurityContextHolder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContext&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;setAuthentication&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&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="n"&gt;filterChain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;doFilter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&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;Because the server stores no session state, every request must be validated independently. The filter re-verifies the signature and the expiry claim on each call. This is what makes JWT genuinely stateless: the token is self-contained, and the server-side footprint is just the public key or shared secret used for verification.&lt;/p&gt;

&lt;p&gt;The filter is also the right place to handle more advanced concerns. Token revocation, for example, can be layered in by checking a blocklist inside the same &lt;code&gt;doFilterInternal&lt;/code&gt; method before the &lt;code&gt;Authentication&lt;/code&gt; object is written to the context. Compromised password detection follows the same pattern. The filter becomes the single choke point for every token lifecycle decision, which keeps your authorisation logic clean and your business controllers unaware of authentication mechanics.&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%2Fh58vblu3jzskubntj34i.jpg" 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%2Fh58vblu3jzskubntj34i.jpg" alt=" " width="799" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  OAuth2 Login &amp;amp; Resource Server: Delegating Trust to an Authorisation Server
&lt;/h2&gt;

&lt;p&gt;Where JWT authentication is a two-party handshake between your application and the token, OAuth2 introduces a third party: an authorisation server that your application has to coordinate with in real time. Spring Security handles all of that coordination, but you need to tell it which role your application is playing, because the configuration and the filter chain behaviour are meaningfully different depending on the answer.&lt;/p&gt;

&lt;p&gt;Spring Security's OAuth2 support draws a clean line between two roles. &lt;code&gt;oauth2Login()&lt;/code&gt; configures your application as an &lt;strong&gt;OAuth2 client&lt;/strong&gt;: it performs the redirect to the authorization server, receives the callback, exchanges the authorization code for tokens, and logs the user in. &lt;code&gt;oauth2ResourceServer()&lt;/code&gt; configures your application as an &lt;strong&gt;API backend&lt;/strong&gt; that accepts and validates bearer tokens issued by an external authorization server. These two modes are not interchangeable. A browser-facing web app typically uses &lt;code&gt;oauth2Login()&lt;/code&gt;; a REST API consumed by other services typically uses &lt;code&gt;oauth2ResourceServer()&lt;/code&gt;. Some architectures need both in separate security filter chains.&lt;/p&gt;

&lt;p&gt;In the Authorisation Code flow, &lt;code&gt;OAuth2LoginAuthenticationFilter&lt;/code&gt; does the heavy lifting on the client side. When the authorisation server redirects the browser back to your app with a &lt;code&gt;?code=...&lt;/code&gt; parameter, this filter intercepts the request, calls the authorisation server's token endpoint to exchange the code for an access token and ID token, loads the user's identity from the user-info endpoint or from the ID token directly, and populates the &lt;code&gt;SecurityContext&lt;/code&gt;. You do not write any of that exchange logic yourself. The minimal configuration to enable it looks like this:&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;@EnableWebSecurity&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="nd"&gt;@Bean&lt;/span&gt;
 &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;SecurityFilterChain&lt;/span&gt; &lt;span class="nf"&gt;filterChain&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpSecurity&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="n"&gt;http&lt;/span&gt;
&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;
&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;anyRequest&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;authenticated&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="na"&gt;oauth2Login&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Customizer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withDefaults&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;http&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 Boot auto-configuration picks up the client registration details from &lt;code&gt;application.yml&lt;/code&gt;, client ID, client secret, authorisation URI, and token URI, so the Java config stays short.&lt;/p&gt;

&lt;p&gt;On the resource server side, &lt;code&gt;oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)&lt;/code&gt; swaps in a &lt;code&gt;BearerTokenAuthenticationFilter&lt;/code&gt; that reads the &lt;code&gt;Authorisation: Bearer...&lt;/code&gt; header, validates the JWT's signature against the authorisation server's published JWKS endpoint, and populates the &lt;code&gt;SecurityContext&lt;/code&gt; with the decoded claims. Introspection, calling the authorisation server's introspect endpoint on every request instead of validating locally, is also supported, and is the right choice when you need real-time token revocation checks rather than relying on a short expiry window.&lt;/p&gt;

&lt;p&gt;Token lifecycle is another area where Spring Security saves work. When a stored access token has expired, the OAuth2 client support automatically attempts a refresh token grant before forwarding the request. Your application layer never sees the expired token.&lt;/p&gt;

&lt;p&gt;One configuration pitfall is worth calling out explicitly. If you want to run custom logic after the token endpoint responds, setting a cookie from the token, for example, that logic must be placed inside the &lt;code&gt;SecurityFilterChain&lt;/code&gt; at the correct position using &lt;code&gt;addFilterAfter()&lt;/code&gt;, not in a plain servlet filter sitting outside Spring Security. Filters registered outside the chain execute at a different point in the request lifecycle and cannot reliably observe the outcome of Spring Security's token processing.&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%2Fvq1a6i0cousezeo9reii.jpg" 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%2Fvq1a6i0cousezeo9reii.jpg" alt=" " width="800" height="671"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  SSO with Spring Security: Session-Backed Identity Federation
&lt;/h2&gt;

&lt;p&gt;The sharpest difference between SSO and the JWT path covered earlier is not in the configuration, it's in what survives a request boundary. JWT authentication is stateless: every request carries its own proof of identity in the &lt;code&gt;Authorization&lt;/code&gt; header, and the filter validates that proof from scratch each time. SSO via &lt;code&gt;oauth2Login()&lt;/code&gt; works the opposite way. Once a user completes the redirect dance with the identity provider, Spring Security stores the resulting &lt;code&gt;Authentication&lt;/code&gt; object in the HTTP session, and every subsequent request is resolved against that session rather than against a fresh token. &lt;/p&gt;

&lt;p&gt;That session-backed model is what makes SSO feel seamless to the end user: they authenticate once with Google or GitHub, get redirected back to your application, and from that point on the browser's session cookie is the credential. The identity provider is not consulted again until the session expires or the user explicitly signs out.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;application.yml&lt;/code&gt; registration for a Google-backed SSO looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
 &lt;span class="na"&gt;security&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
 &lt;span class="na"&gt;oauth2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
 &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
 &lt;span class="na"&gt;registration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
 &lt;span class="na"&gt;google&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
 &lt;span class="na"&gt;client-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;YOUR_CLIENT_ID&lt;/span&gt;
 &lt;span class="na"&gt;client-secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;YOUR_CLIENT_SECRET&lt;/span&gt;
 &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openid, profile, email&lt;/span&gt;
 &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
 &lt;span class="na"&gt;google&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
 &lt;span class="na"&gt;authorization-uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://accounts.google.com/o/oauth2/v2/auth&lt;/span&gt;
 &lt;span class="na"&gt;token-uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://oauth2.googleapis.com/token&lt;/span&gt;
 &lt;span class="na"&gt;user-info-uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://www.googleapis.com/oauth2/v3/userinfo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Java security configuration that activates this is identical to the OAuth2 Login example in the previous section, &lt;code&gt;http.oauth2Login()&lt;/code&gt; is all that's needed, with no additional filter registration. What changes is what happens inside the filter chain after the callback completes.&lt;/p&gt;

&lt;p&gt;In the JWT path, &lt;code&gt;SecurityContextHolderFilter&lt;/code&gt; reconstructs the &lt;code&gt;SecurityContext&lt;/code&gt; on every request by validating the bearer token. In the SSO path, the same filter reconstructs the &lt;code&gt;SecurityContext&lt;/code&gt; by reading it from the &lt;code&gt;HttpSession&lt;/code&gt;, no token validation occurs because the session already holds a fully populated &lt;code&gt;OAuth2AuthenticationToken&lt;/code&gt;. That is the meaningful delta: the persistence strategy changes from token-per-request to session-per-user, and &lt;code&gt;SecurityContextHolderFilter&lt;/code&gt; adapts accordingly through &lt;code&gt;HttpSessionSecurityContextRepository&lt;/code&gt; rather than the stateless repository wired in the JWT configuration.&lt;/p&gt;

&lt;p&gt;The older &lt;code&gt;@EnableOAuth2Sso&lt;/code&gt; annotation from the Spring Security OAuth2 legacy project achieved the same redirect-and-session behaviour, but required you to manually wire an &lt;code&gt;OAuth2ClientContext&lt;/code&gt; and build an &lt;code&gt;OAuth2ClientAuthenticationProcessingFilter&lt;/code&gt; yourself. Spring Security 5's native &lt;code&gt;oauth2Login()&lt;/code&gt; DSL absorbed all of that plumbing, which is why a single YAML block and one DSL method call are now sufficient for a fully working SSO integration against Google, GitHub, or any standards-compliant identity provider. &lt;/p&gt;

&lt;p&gt;One practical implication worth naming: because the session holds the &lt;code&gt;Authentication&lt;/code&gt;, horizontal scaling requires either sticky sessions or a shared session store like Redis. This is the trade-off you accept relative to the stateless JWT model, where any node can validate a request independently.&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%2Fjz9dh10cal2uil6lf0k1.jpg" 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%2Fjz9dh10cal2uil6lf0k1.jpg" alt=" " width="800" height="768"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing the Three Mechanisms: Which Hook, Which Flow, Which Config
&lt;/h2&gt;

&lt;p&gt;The three token mechanisms are not strict alternatives to one another, they solve slightly different problems, and they plug into the filter chain at different points. Knowing which slot each one occupies is the fastest way to choose between them and to diagnose failures when they occur.&lt;/p&gt;

&lt;p&gt;JWT authentication is the right choice for stateless APIs and microservices where no session should be kept server-side. Your application is the sole verifier of the token: it checks the signature, extracts claims, and populates the &lt;code&gt;SecurityContext&lt;/code&gt;, all within a single &lt;code&gt;OncePerRequestFilter&lt;/code&gt; placed before &lt;code&gt;UsernamePasswordAuthenticationFilter&lt;/code&gt;. Nothing about the user's identity is stored between requests.&lt;/p&gt;

&lt;p&gt;OAuth2 resource server configuration is appropriate when a separate authorisation server issues tokens, and your application should validate them against a well-known JWKS endpoint. Spring's built-in &lt;code&gt;BearerTokenAuthenticationFilter&lt;/code&gt; handles the extraction and delegates to the &lt;code&gt;JwtDecoder&lt;/code&gt; you configure, so you write almost no filter code yourself. The trust anchor is the authorisation server's public key, not anything your application generates.&lt;/p&gt;

&lt;p&gt;SSO via &lt;code&gt;oauth2Login&lt;/code&gt; applies when a browser-based user needs to authenticate once and have that identity recognised across multiple applications. It reuses the same &lt;code&gt;OAuth2LoginAuthenticationFilter&lt;/code&gt; that handles a standard OAuth2 login callback, but the session becomes the persistence mechanism rather than a token the client presents on each request. The identity provider, not your application, manages when that session expires.&lt;/p&gt;

&lt;p&gt;When a 401 appears, and you need to find out why, the filter chain position is almost always the first thing worth examining. In a JWT setup, a silent 401 with no accompanying error body usually means one of two things: the custom filter ran but did not write to the &lt;code&gt;SecurityContext&lt;/code&gt;, or the filter was registered at a position that places it after the authorisation check rather than before it. In an OAuth2 resource server setup, the failure is more often a misconfigured &lt;code&gt;issuer-uri&lt;/code&gt; or an expired token that the decoder rejects. In an SSO setup, a 302 redirect rather than a 401 is the more common symptom; the session wasn't found, so the user is sent back to the identity provider.&lt;/p&gt;

&lt;p&gt;The filter chain is the right mental model to carry into all three of these situations. Once you see JWT validation, OAuth2 token introspection, and SSO session lookup as separate plug-ins wired into the same ordered pipeline, the framework stops behaving like an unpredictable wall and starts behaving like a predictable sequence you can reason about step by step. Every 401 is answerable by asking: which filter was responsible for this request, did it run, and if it ran, did it write to the &lt;code&gt;SecurityContext&lt;/code&gt;? You can make that question concrete immediately by enabling &lt;code&gt;TRACE&lt;/code&gt; logging on &lt;code&gt;org.springframework.security&lt;/code&gt;, the output prints each filter in the chain as it executes, which turns a confusing status code into a visible gap in the sequence. That visibility is a direct consequence of the model: when you understand the pipeline, you know exactly where to look.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://github.com/hardikSinghBehl/jwt-auth-flow-spring-security" rel="noopener noreferrer"&gt;GitHub - hardikSinghBehl/jwt-auth-flow-spring-security: Java backend application using Spring-security to implement JWT based Authentication and Authorization · GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/questions/58331184/spring-security-oauth2-add-filter-after-oauth-token-call" rel="noopener noreferrer"&gt;java - Spring security oauth2 - add filter after oauth/token call - Stack Overflow&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
    </item>
    <item>
      <title>How Spring Data JPA, JPA, and Hibernate work together</title>
      <dc:creator>srinivas reddy gouru</dc:creator>
      <pubDate>Tue, 26 May 2026 17:24:05 +0000</pubDate>
      <link>https://dev.to/srinivas_gouru_d26dc31f21/how-spring-data-jpa-jpa-and-hibernate-work-together-55cn</link>
      <guid>https://dev.to/srinivas_gouru_d26dc31f21/how-spring-data-jpa-jpa-and-hibernate-work-together-55cn</guid>
      <description>&lt;h2&gt;
  
  
  The Magic Line That Raises the Right Question
&lt;/h2&gt;

&lt;p&gt;Spring Data JPA is a library that lets you query a relational database by writing a Java interface method and nothing else: no SQL, no &lt;code&gt;ResultSet&lt;/code&gt; parsing, no &lt;code&gt;PreparedStatement&lt;/code&gt; boilerplate. You declare what you want, and the framework figures out how to fetch it.&lt;/p&gt;

&lt;p&gt;Here is the moment that stops most backend engineers in their tracks:&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="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;UserRepository&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;JpaRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findByEmail&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;email&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;That is the entire implementation. You inject &lt;code&gt;UserRepository&lt;/code&gt; into a service, call &lt;code&gt;userRepository.findByEmail("alice@example.com")&lt;/code&gt;, and get back a populated &lt;code&gt;User&lt;/code&gt; object from the database. [1] The method works on the first try, which is satisfying until something breaks, or until you need to understand why a query is slow, why a &lt;code&gt;LazyInitializationException&lt;/code&gt; keeps appearing, or why your transaction did not roll back the way you expected.&lt;/p&gt;

&lt;p&gt;At that point, the magic stops feeling helpful and starts feeling like a wall.&lt;/p&gt;

&lt;p&gt;The wall exists because &lt;code&gt;findByEmail&lt;/code&gt; is not a single thing. It is the visible surface of three separate layers, each with its own responsibilities, failure modes, and configuration surface. Spring Data JPA translated your method name into a query. JPA, the Java Persistence API, provided the standard contract that queries had to follow. Hibernate actually executed it against the database. Without knowing which layer owns which job, you cannot know which layer to look at when something goes wrong.&lt;/p&gt;

&lt;p&gt;To answer why that one line works, you need a clear picture of all three layers. That is what the rest of this article builds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Layers, Three Jobs: The Stack in One Mental Model
&lt;/h2&gt;

&lt;p&gt;When you call &lt;code&gt;findByEmail("alice@example.com")&lt;/code&gt; on a repository interface you never implemented, what actually runs? The answer involves three distinct layers, each with a single clear job, and conflating any two of them is the source of most of the confusion engineers run into.&lt;/p&gt;

&lt;p&gt;At the bottom sits &lt;strong&gt;Hibernate&lt;/strong&gt;. Hibernate is a full ORM framework. It owns a &lt;code&gt;SessionFactory&lt;/code&gt;, manages an in-memory representation of your entities, generates SQL, and fires that SQL over JDBC to the database. [2] When things go wrong at the database level, a bad join, an unexpected number of queries, or a lazy-loading exception, Hibernate is the layer to examine.&lt;/p&gt;

&lt;p&gt;In the middle sits &lt;strong&gt;JPA&lt;/strong&gt; (Java Persistence API). JPA is not a library you can download and run; it is a specification, a set of interfaces, annotations, and contracts that any compliant ORM must honour. [3] It defines what &lt;code&gt;@Entity&lt;/code&gt;, &lt;code&gt;@OneToMany&lt;/code&gt;, and &lt;code&gt;EntityManager&lt;/code&gt; mean, but it ships no implementation of its own. Hibernate is Spring Boot's default JPA provider, meaning Hibernate is the concrete code that fulfils those contracts. [2] This distinction matters because your annotations belong to JPA, while the SQL they produce belongs to Hibernate.&lt;/p&gt;

&lt;p&gt;At the top sits &lt;strong&gt;Spring Data JPA&lt;/strong&gt;. Its sole job is to eliminate boilerplate in the data-access layer. [1] You write a repository interface; Spring generates a proxy implementation at startup that translates method names and annotations into JPA operations. [1] Spring Data JPA does not talk to your database directly, and it is not itself an ORM. Every call it receives flows downward through JPA's &lt;code&gt;EntityManager&lt;/code&gt; and on into Hibernate, which finally issues the JDBC call. &lt;/p&gt;

&lt;p&gt;Laid out as a delegation chain, the flow looks 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;Your code
 → Spring Data JPA (repository proxy)
 → JPA EntityManager (standard contract)
 → Hibernate (SQL generation + JDBC)
 → Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each arrow in that chain crosses a responsibility boundary. Spring Data JPA knows about Spring conventions and method-name patterns. JPA knows about entities and the persistence context lifecycle. Hibernate knows about SQL dialects and connection pooling. None of the three layers was designed to do each other's jobs, which is exactly why each one is replaceable in theory. You could swap Hibernate for EclipseLink and keep all your JPA annotations untouched, or swap Spring Data JPA for plain &lt;code&gt;EntityManager&lt;/code&gt; calls and keep Hibernate doing exactly what it already does.&lt;/p&gt;

&lt;p&gt;Keeping this map in your head pays off the moment something goes wrong. A &lt;code&gt;LazyInitializationException&lt;/code&gt; is a Hibernate story. A &lt;code&gt;@Transactional&lt;/code&gt; annotation that seems to do nothing is a Spring story. A repository method that generates a query you didn't expect is a Spring Data JPA story, though Hibernate ultimately executes it. The next three sections go bottom-up through the stack, starting with the layer that actually touches your database.&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%2Fghrdrxsi72k8we0xo79j.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%2Fghrdrxsi72k8we0xo79j.png" alt=" " width="800" height="2443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Hibernate: The Engine That Actually Talks to the Database
&lt;/h2&gt;

&lt;p&gt;When your Spring application saves an entity and an SQL &lt;code&gt;INSERT&lt;/code&gt; appears in the logs, Hibernate wrote that statement. It is an Object-Relational Mapping framework whose single job is bridging the gap between Java objects and relational tables, translating method calls and object state into the SQL your database actually understands, so you don't have to hand-write every query for routine CRUD operations. [2]&lt;/p&gt;

&lt;p&gt;At the centre of Hibernate's runtime is the &lt;code&gt;SessionFactory&lt;/code&gt;, a heavyweight, thread-safe object built once at application startup. From that factory, Hibernate creates per-request &lt;code&gt;Session&lt;/code&gt; instances (or, in the JPA vocabulary you'll see more often in Spring code, &lt;code&gt;EntityManager&lt;/code&gt; instances). Those per-request objects are not thread-safe, so Spring handles their lifecycle carefully. The &lt;code&gt;EntityManager&lt;/code&gt; you inject with &lt;code&gt;@PersistenceContext&lt;/code&gt; is actually a thread-bound proxy that delegates to whichever transactional &lt;code&gt;EntityManager&lt;/code&gt; is currently active for that request, falling back to a freshly created one if no transaction is in progress. [4] That indirection is invisible in normal usage but matters when you start debugging concurrency issues.&lt;/p&gt;

&lt;p&gt;Several behaviours that surface as runtime surprises are owned entirely by Hibernate, not by the layers above it. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dirty checking&lt;/strong&gt; is one of the most useful and least understood. Inside an active transaction, Hibernate holds a snapshot of every entity it has loaded. When the transaction commits, it compares current field values against those snapshots and issues &lt;code&gt;UPDATE&lt;/code&gt; statements automatically for anything that changed, even if you never called &lt;code&gt;save()&lt;/code&gt;. This is a feature, but it confuses engineers who expect no SQL to run unless they explicitly persist something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First-level cache&lt;/strong&gt; (the session cache) means that within a single &lt;code&gt;EntityManager&lt;/code&gt; lifetime, calling &lt;code&gt;find(User.class, 1L)&lt;/code&gt; twice returns the same Java object from memory on the second call, with no second database round-trip. This cache is scoped to the session, not the application, so it disappears when the session closes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lazy loading&lt;/strong&gt; lets Hibernate defer fetching associated collections until your code actually accesses them. A &lt;code&gt;User&lt;/code&gt; with a &lt;code&gt;@OneToMany&lt;/code&gt; list of &lt;code&gt;Orders&lt;/code&gt; won't load those orders until you call &lt;code&gt;user.getOrders()&lt;/code&gt;. The downside is the N+1 problem: if you load 50 users in a list and then touch &lt;code&gt;.getOrders()&lt;/code&gt; for each one inside a loop, Hibernate fires one query to fetch the users and then 50 separate queries to fetch each user's orders. The fix usually involves a &lt;code&gt;JOIN FETCH&lt;/code&gt; in your query or switching the fetch type, but the first step is recognising that Hibernate is what's generating those 50 extra statements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SQL dialect translation&lt;/strong&gt; means Hibernate handles the differences between database engines. You configure &lt;code&gt;spring.jpa.database-platform&lt;/code&gt; to point at a dialect class, and Hibernate adapts its generated SQL to match the syntax of PostgreSQL, MySQL, Oracle, or whatever you're running.&lt;/p&gt;

&lt;p&gt;All of these behaviours, dirty checking, caching, lazy loading, dialect generation, live at the Hibernate layer. When something breaks, the explanation usually lives there too. The layer above it, JPA, doesn't implement any of this; it just defines the contract that Hibernate must honour.&lt;/p&gt;

&lt;h2&gt;
  
  
  JPA: The Standard Contract Every ORM Must Honour
&lt;/h2&gt;

&lt;p&gt;The previous section covered what Hibernate actually does at runtime. But if you look at a typical Spring Boot entity class, you'll notice the imports don't say &lt;code&gt;org.hibernate.*&lt;/code&gt;. They say &lt;code&gt;jakarta.persistence.*&lt;/code&gt; (or &lt;code&gt;javax.persistence.*&lt;/code&gt; on older versions). That's JPA, and understanding why it's there clarifies the whole stack.&lt;/p&gt;

&lt;p&gt;JPA, the Jakarta Persistence API, is a specification, not a library you run. It defines a set of interfaces, annotations, and rules that any compliant ORM must implement. [2] The annotations you use every day, &lt;code&gt;@Entity&lt;/code&gt;, &lt;code&gt;@Id&lt;/code&gt;, &lt;code&gt;@OneToMany&lt;/code&gt;, &lt;code&gt;@Column&lt;/code&gt;, are all defined by JPA. The central API for interacting with the persistence context, &lt;code&gt;EntityManager&lt;/code&gt;, is also defined by JPA, with methods like &lt;code&gt;persist&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt;, &lt;code&gt;merge&lt;/code&gt;, and &lt;code&gt;remove&lt;/code&gt;. Hibernate is simply the most popular implementation of that contract.&lt;/p&gt;

&lt;p&gt;This separation matters practically. Because your application code targets JPA interfaces rather than Hibernate-specific classes, you could swap Hibernate for EclipseLink or OpenJPA and recompile without touching your entity or service code. In practice, most teams never make that swap, but portability isn't the only benefit. The bigger win is that JPA gives the whole Java ecosystem a shared vocabulary. Documentation, Stack Overflow answers, and framework libraries all speak JPA, even when each is backed by a different provider.&lt;/p&gt;

&lt;p&gt;JPA also introduced JPQL, the Java Persistence Query Language, which lets you query against entity class names and field names rather than table and column names. A JPQL query looks like this:&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="nc"&gt;TypedQuery&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entityManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createQuery&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
 &lt;span class="s"&gt;"SELECT o FROM Order o WHERE o.customer.email =:email"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;
&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setParameter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"alice@example.com"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getResultList&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare that to raw JDBC, where you would write a SQL string against &lt;code&gt;orders.customer_email&lt;/code&gt;, manually open a &lt;code&gt;ResultSet&lt;/code&gt;, and map each column back to a field by hand. [5] JPQL keeps queries at the object level, so renaming a Java field and its corresponding column mapping stays in one place rather than scattered across SQL strings throughout the codebase.&lt;/p&gt;

&lt;p&gt;The honest limitation of plain JPA is that it still requires you to wire up an &lt;code&gt;EntityManagerFactory&lt;/code&gt;, manage &lt;code&gt;EntityManager&lt;/code&gt; lifecycles, write those &lt;code&gt;createQuery&lt;/code&gt; calls, and handle transactions explicitly. For a small application that's manageable. For a service with thirty entity types and hundreds of queries, it becomes a lot of repeated structural code. That gap is exactly what Spring Data JPA was designed to close.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spring Data JPA: The Boilerplate Eliminator on Top
&lt;/h2&gt;

&lt;p&gt;With the JPA specification and Hibernate's implementation both accounted for, the question that opened this article is still hanging: where exactly does &lt;code&gt;findByEmail()&lt;/code&gt; come from? You never wrote a SQL query, never touched an &lt;code&gt;EntityManager&lt;/code&gt;, never defined a concrete class. The answer lives entirely in Spring Data JPA's repository abstraction.&lt;/p&gt;

&lt;p&gt;When your application starts up, Spring Data JPA scans for interfaces that extend &lt;code&gt;JpaRepository&lt;/code&gt; (or one of its parent interfaces) and generates concrete implementations on the fly. [1] No DAO class needs to be written, because Spring creates a proxy object that wires in the actual behaviour at startup, before any request arrives. That proxy is what gets injected into your service layer when you use &lt;code&gt;@Autowired&lt;/code&gt; or constructor injection.&lt;/p&gt;

&lt;p&gt;The method-name translation is the part that feels most like magic. Spring Data JPA parses the method signature using reflection, breaks it down by the keywords it recognises (&lt;code&gt;findBy&lt;/code&gt;, &lt;code&gt;And&lt;/code&gt;, &lt;code&gt;Or&lt;/code&gt;, &lt;code&gt;OrderBy&lt;/code&gt;, &lt;code&gt;Between&lt;/code&gt;, and so on), and constructs a JPQL query to match. A method named &lt;code&gt;findByLastNameAndAgeGreaterThan&lt;/code&gt; becomes something like &lt;code&gt;SELECT u FROM User u WHERE u.lastName =:lastName AND u.age &amp;gt;:age&lt;/code&gt;, assembled at startup rather than at call time. If you make a typo in the method name that produces an unresolvable expression, the application will fail to start, not fail at 2 AM when that code path is first hit 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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;UserRepository&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;JpaRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findByLastName&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;lastName&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
 &lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findByEmail&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;email&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
 &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findByAgeGreaterThanOrderByLastNameAsc&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;age&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;That is the entire file. Spring Data JPA does the rest.&lt;/p&gt;

&lt;p&gt;When one of these methods is called at runtime, the proxy delegates to a &lt;code&gt;SimpleJpaRepository&lt;/code&gt; implementation under the hood, which in turn uses a JPA &lt;code&gt;EntityManager&lt;/code&gt; to execute the translated query. That &lt;code&gt;EntityManager&lt;/code&gt; call flows down to Hibernate, which generates the SQL and sends it to the database. The call chain traverses all three layers: your repository interface, the JPA &lt;code&gt;EntityManager&lt;/code&gt;, and Hibernate's SQL engine.&lt;/p&gt;

&lt;p&gt;Spring Data JPA is part of the broader Spring Data umbrella project, which applies the same repository abstraction pattern to MongoDB, Redis, Cassandra, and other stores. Switching from a relational database to MongoDB doesn't require learning an entirely different programming model. You extend a MongoDB-specific repository interface, and the query derivation works the same way. That consistency is the deeper goal of Spring Data JPA's design.&lt;/p&gt;

&lt;p&gt;Derived query methods cover a large surface area, but they are not the only option. For anything complex, &lt;code&gt;@Query&lt;/code&gt; lets you write JPQL (or even native SQL with &lt;code&gt;nativeQuery = true&lt;/code&gt;) directly on the method. The proxy mechanism stays in place; only the query source changes. This means you can stay in the repository interface without writing any implementation code, even for queries that method-name derivation cannot express cleanly.&lt;/p&gt;

&lt;p&gt;Knowing which layer owns which behaviour is immediately useful when things go wrong.&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%2Fzs8v067fp46vvjd1dj20.jpg" 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%2Fzs8v067fp46vvjd1dj20.jpg" alt=" " width="799" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  When the Magic Breaks: Debugging Across the Three Layers
&lt;/h2&gt;

&lt;p&gt;Each failure mode has a home layer, and pointing your debugger at the wrong one wastes time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;LazyInitializationException&lt;/code&gt;, Hibernate's session is closed&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This exception means Hibernate tried to load a lazy association, looked for an open persistence context, and found none. [4] It is a Hibernate-layer error about session lifecycle, not a Spring Data JPA bug. The usual cause is accessing a &lt;code&gt;@OneToMany&lt;/code&gt; collection after the repository method has returned and the &lt;code&gt;EntityManager&lt;/code&gt; has closed. [2] Fix it at the layer that owns sessions: either annotate the calling service method with &lt;code&gt;@Transactional&lt;/code&gt; so the persistence context stays open while you traverse the association, or switch the fetch type to eager for relationships you always need. A third option is a JPQL &lt;code&gt;JOIN FETCH&lt;/code&gt; or &lt;code&gt;@EntityGraph&lt;/code&gt; on the repository method itself, which tells Hibernate to load the association in the initial query rather than deferring it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unexpected query count, Hibernate's lazy-loading strategy&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your application issues far more SQL statements than you expect, the N+1 problem is the likely culprit. Hibernate's default for &lt;code&gt;@OneToMany&lt;/code&gt; is &lt;code&gt;FetchType.LAZY&lt;/code&gt;, so fetching ten authors and iterating their posts triggers one query for the author list and ten more for the post collections. [6] You won't see this in your repository interface; you have to look at what Hibernate is generating. Enable SQL logging with these two lines in &lt;code&gt;application.properties&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;spring.jpa.show-sql&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;logging.level.org.hibernate.SQL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;DEBUG&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you can see the queries, the fix is at the Hibernate or JPA layer, not the Spring Data layer. A &lt;code&gt;JOIN FETCH&lt;/code&gt; in a &lt;code&gt;@Query&lt;/code&gt; annotation collapses the N+1 into a single join. [6] &lt;code&gt;@EntityGraph&lt;/code&gt; on the repository method achieves the same result declaratively. [6] For cases where you prefer to keep lazy loading but still want fewer round trips, setting &lt;code&gt;spring.jpa.properties.hibernate.default_batch_fetch_size=10&lt;/code&gt; tells Hibernate to load related collections in batches with an &lt;code&gt;IN&lt;/code&gt; clause rather than one query per row. [6]&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transaction rollback surprises, Spring's &lt;code&gt;@Transactional&lt;/code&gt; infrastructure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a transaction does not behave the way you expect, changes not persisting, rollbacks firing on the wrong exception, or operations on a second data source not participating in the transaction, the problem lives at the Spring layer. &lt;code&gt;@Transactional&lt;/code&gt; is managed entirely by Spring's AOP proxy, and its default behaviour is to roll back only on unchecked exceptions. If your application has multiple data sources, Spring applies &lt;code&gt;@Transactional&lt;/code&gt; to the primary one by default; the secondary data source participates only if you configure a &lt;code&gt;JtaTransactionManager&lt;/code&gt; or explicitly name the transaction manager in the annotation. [7] The fix is a Spring configuration change, not a Hibernate tuning exercise.&lt;/p&gt;

&lt;p&gt;The mental model from earlier sections makes all three of these straightforward to triage: session lifecycle is Hibernate, query shape is Hibernate, transaction boundaries are Spring. When you see a stack trace or unexpected behaviour, ask which layer owns that concept, then inspect or configure exactly that layer. Turning on SQL logging in development is a good habit regardless; the raw queries Hibernate sends are the single most informative signal available, and most performance surprises reveal themselves within a few minutes of reading them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://spring.io/projects/spring-data-jpa" rel="noopener noreferrer"&gt;Spring Data JPA&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/@burakkocakeu/jpa-hibernate-and-spring-data-jpa-efa71feb82ac" rel="noopener noreferrer"&gt;JPA, Hibernate And Spring Data JPA&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.baeldung.com/spring-data-jpa-vs-jpa" rel="noopener noreferrer"&gt;Difference Between JPA and Spring Data JPA | Baeldung&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.spring.io/spring-framework/reference/data-access/orm/jpa.html" rel="noopener noreferrer"&gt;JPA :: Spring Framework&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.baeldung.com/jpa-vs-jdbc" rel="noopener noreferrer"&gt;A Comparison Between JPA and JDBC | Baeldung&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/sadiul_hakim/understanding-and-solving-the-n1-problem-in-spring-data-jpa-2b6f"&gt;Understanding and Solving the N+1 Problem in Spring Data JPA - DEV Community&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/questions/23862994/whats-the-difference-between-hibernate-and-spring-data-jpa" rel="noopener noreferrer"&gt;java - What's the difference between Hibernate and Spring Data JPA - Stack Overflow&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>backend</category>
      <category>database</category>
      <category>java</category>
      <category>springboot</category>
    </item>
  </channel>
</rss>
