Real World Example

A Wallet App (Paytm / PhonePe Style)

Money-moving services get the strictest rules. A typical layered setup:

LayerControl
EdgeWAF + rate limits per device and per user; JWT validated at the gateway; device binding
wallet-serviceNeeds scope wallet:debit AND a fresh step-up auth claim (UPI PIN verified in the last 2 min)
Between servicesmTLS by the service mesh; client-credentials tokens for background jobs
DataCard/bank data in a separate PCI-DSS zone; secrets from Vault; audit log of every debit

wallet-service Security Configuration

@Configuration
@EnableMethodSecurity
public class WalletSecurityConfig {

    @Bean
    SecurityFilterChain wallet(HttpSecurity http) throws Exception {
        return http
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/actuator/health/**").permitAll()
                        .requestMatchers(HttpMethod.GET, "/api/wallet/balance").hasAuthority("SCOPE_wallet:read")
                        .requestMatchers(HttpMethod.POST, "/api/wallet/debit").hasAuthority("SCOPE_wallet:debit")
                        .anyRequest().denyAll())                                   // deny by default
                .oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()))
                .build();
    }
}

@RestController
@RequestMapping("/api/wallet")
@RequiredArgsConstructor
public class WalletController {
    private final WalletService wallet;

    @PostMapping("/debit")
    @PreAuthorize("@stepUp.isRecent(authentication, T(java.time.Duration).ofMinutes(2))")
    public DebitResponse debit(@RequestHeader("Idempotency-Key") String key,
                               @Valid @RequestBody DebitRequest req,
                               @AuthenticationPrincipal Jwt jwt) {
        return wallet.debit(jwt.getSubject(), req, key);        // subject from the token, never from the body
    }
}

Tip: Notice two habits: deny by default (anyRequest().denyAll()), and take the user ID from the verified token (jwt.getSubject()), never from a request field that the client controls.