<?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: Federico Herrera</title>
    <description>The latest articles on DEV Community by Federico Herrera (@fdherrera).</description>
    <link>https://dev.to/fdherrera</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%2F3607651%2F0645b75a-d7a5-4538-b6e4-f5f1de4229df.png</url>
      <title>DEV Community: Federico Herrera</title>
      <link>https://dev.to/fdherrera</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fdherrera"/>
    <language>en</language>
    <item>
      <title>Llegó MFA con Spring Security 7</title>
      <dc:creator>Federico Herrera</dc:creator>
      <pubDate>Tue, 23 Dec 2025 14:00:00 +0000</pubDate>
      <link>https://dev.to/fdherrera/llego-mfa-con-spring-security-7-m6e</link>
      <guid>https://dev.to/fdherrera/llego-mfa-con-spring-security-7-m6e</guid>
      <description>&lt;p&gt;Desde su salida, Spring Security no ofreció soporte nativo para Multi-Factor Authentication (MFA). Implementar un segundo factor como un código temporal, de esos que te mandan un codigo por SMS o Whatsapp, una app autenticadora o un token, implicaba crear filtros personalizados, &lt;code&gt;AuthenticationProvider&lt;/code&gt;s propios y manejar estados intermedios de autenticación a mano. Cada implementación terminaba siendo distinta.&lt;/p&gt;

&lt;p&gt;Finalmente, Spring Security 7 lo hizo. El framework introduce soporte oficial y declarativo para MFA, integrándolo directamente en su modelo de autenticación y autorización. Ahora, cada factor autenticado se representa como una autoridad, permitiendo definir reglas de acceso que exigen múltiples factores sin necesidad de lógica personalizada ni flujos improvisados.&lt;/p&gt;

&lt;p&gt;Ahora vamos a ver qué resuelve este nuevo soporte y cómo implementar MFA de manera simple y consistente.&lt;/p&gt;

&lt;p&gt;Naturalmente, la fuente final de la verdad es la &lt;a href="https://docs.spring.io/spring-security/reference/servlet/authentication/mfa.html" rel="noopener noreferrer"&gt;documentación oficial&lt;/a&gt;, que se mantiene actualizada por los desarrolladores y la comunidad.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué es MFA?
&lt;/h3&gt;

&lt;p&gt;Hay un montón de recursos online para responder esta pregunta así que a efectos de este post lo vamos a simplificar, si quieres profundizar, consúltale a Google o a tu IA de confianza.&lt;/p&gt;

&lt;p&gt;Básicamente, MFA (Multi-Factor Authentication) es &lt;strong&gt;un mecanismo de autenticación&lt;/strong&gt; que requiere de 2 o más formas de probar que eres quien dices ser para acceder a un recurso o sección de una aplicación. La forma más común, y que posiblemente ya conozcas, de esta técnica, es cuando al loguearte a una aplicación usando tu usuario y contraseña o tu cuenta de mail, necesites, además, validar un código que el sistema te envía a tu celular mediante SMS o Whatsapp o que hagas click en un enlace que te envien por cualquier medio. Otras formas involucran biometría, como tu huella dactilar o escáner facial, en la mayoria de dispositivos moviles, o una llave física, por lo general la empresa para la que trabajes puede llegarte a brindar uno de estos dispositivos.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Cómo lo implemento en mi proyecto?
&lt;/h3&gt;

&lt;p&gt;Sin mas preámbulo, vamos a ver cómo implementar estos mecanismos en tu aplicación BE Spring.&lt;/p&gt;

&lt;p&gt;Para no ser una duplicado de la guía de la documentación voy a hacer leverage de uno de los estándares más usados para autenticación, JWT.&lt;br&gt;
Y es que por diseño Spring Security hace uso de sesiones, véase &lt;a href="https://docs.spring.io/spring-security/reference/servlet/authentication/session-management.html" rel="noopener noreferrer"&gt;Session Management&lt;/a&gt;, pero la mayoría de las veces mantenemos RESTful APIs y necesitamos mantenerlas stateless para asegurar consistencia.&lt;/p&gt;

&lt;p&gt;Un estándar muy utilizado es hacer uso de &lt;strong&gt;tokens&lt;/strong&gt;, donde especificamos las capacidades del usuario una vez se autentica y este lo utiliza como tarjeta de entrada para acceder a los recursos del sistema.&lt;/p&gt;

&lt;p&gt;Planteamos los siguientes requerimientos:&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.amazonaws.com%2Fuploads%2Farticles%2Fl3bjgy9gy5on563tvepn.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%2Fl3bjgy9gy5on563tvepn.png" alt="Diagrama de los requerimientos a implementar" width="800" height="569"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  POST /login
&lt;/h4&gt;

&lt;p&gt;En caso de que sean las credenciales válidas, construimos el token, caso contrario retornamos.&lt;/p&gt;
&lt;h4&gt;
  
  
  GET /user/settings
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Solo usuarios autenticados pueden acceder al recurso.&lt;/li&gt;
&lt;li&gt;Autencicación por token soportada.&lt;/li&gt;
&lt;li&gt;En caso de que el cliente provea un token y sea válido, puede acceder al recurso. Caso contrario retornamos.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Muy bien, para esto podemos hacer algo bastante simple en Spring, primeramente necesitamos algo que nos permita construir el token y decodificarlo/validarlo.&lt;/p&gt;

&lt;p&gt;Para esta demo hago uso de las implementaciones estándar de Spring con certificados estáticos, algo que no es la mejor de las prácticas ni mucho menos recomendable para producción, pero didácticamente nos va a ayudar a reducir el , no voy a entrar en detalle de las implementaciones, pero puedes revisar el código del proyecto si te da curiosidad.&lt;/p&gt;

&lt;p&gt;Entonces configuramos 2 beanes para llevar esto acabo, un &lt;strong&gt;NimbusJwtEncoder&lt;/strong&gt; y un &lt;strong&gt;NimbusJwtDecoder&lt;/strong&gt;, implementaciones estándar de Spring.&lt;/p&gt;

&lt;p&gt;Además, necesitamos un usuario para pruebas, por lo que vamos a crear un service que tenga un usuario de pruebas en memoria.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  @Bean
  UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
    List&amp;lt;UserDetails&amp;gt; users =
        List.of(
            new User(
                "user",
                passwordEncoder.encode("password"),
                List.of(new SimpleGrantedAuthority("ROLE_USER"))));
    return new InMemoryUserDetailsManager(users);
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Debemos decirle a Spring que un usuario va a poder autenticarse usando información que tenemos en el sistema, en producción seria una BBDD, para este caso in-memory. Esto lo conseguimos simplemente definiendo un &lt;code&gt;AuthenticationProvider&lt;/code&gt; que haga uso de algo así: Spring nos entrega &lt;a href="https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/dao-authentication-provider.html" rel="noopener noreferrer"&gt;DaoAuthenticationProvider&lt;/a&gt; para esto.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  @Bean
  AuthenticationManager authenticationManager(
      UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
    DaoAuthenticationProvider authenticationProvider =
        new DaoAuthenticationProvider(userDetailsService);
    authenticationProvider.setPasswordEncoder(passwordEncoder);

    return new ProviderManager(authenticationProvider);
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vemos que éste depende de un PasswordEncoder, además del UserDetailsService que definimos antes, de nuevo, todo está escrito.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  @Bean
  PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este es todo nuestro setup, ahora solo debemos habilitar a cliente a hacer uso del servicio, un controller como este es suficiente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Slf4j
@RestController
@RequiredArgsConstructor
public class AuthController {
  private final AuthenticationManager authenticationManager;
  private final JwtEncoder jwtEncoder;

  @PostMapping(value = "/login", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
  @ResponseStatus(HttpStatus.NO_CONTENT)
  ResponseEntity&amp;lt;Void&amp;gt; login(@RequestParam String username, @RequestParam String password) {
    var authRequest = new UsernamePasswordAuthenticationToken(username, password);
    log.info("Login: {}", authRequest);
    var auth = authenticationManager.authenticate(authRequest);
    String token =
        jwtEncoder
            .encode(
                JwtEncoderParameters.from(
                    JwsHeader.with(SignatureAlgorithm.RS256).build(),
                    JwtClaimsSet.builder()
                        .subject(auth.getName())
                        .claim(
                            SCOPE,
                            auth.getAuthorities().stream()
                                .map(GrantedAuthority::getAuthority)
                                .toList())
                        .expiresAt(Instant.now().plusSeconds(1000))
                        .build()))
            .getTokenValue();
    return ResponseEntity.noContent().header(AUTHORIZATION, "Bearer %s".formatted(token)).build();
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Una capa de servicio podría facilitar la segmentación pero a efectos de esta demo podemos tenerlo todo en el controlador.&lt;/p&gt;

&lt;p&gt;Vamos por partes aquí, primero nos traemos el &lt;code&gt;AuthenticationManager&lt;/code&gt;, como tenemos el método de autenticación por usuario y contraseña, creamos una instancia de &lt;code&gt;UsernamePasswordAuthenticationToken&lt;/code&gt; que luego podemos pasar al auth provider, éste se va a encargar de decirnos si es válido.&lt;br&gt;
Si pasa es porque las credenciales son correctas, por lo que procedemos a construir el token especificando el algoritmo de la firma, el subject que es el mismo username del usuario y agregamos las claims &lt;strong&gt;authorities&lt;/strong&gt; del usuario, que nos permite despues saber  cosas como el rol y cómo se autenticó. Ésta es la forma en la que Spring Security sabe a qué cosas puede acceder el usuario. &lt;a href="https://docs.spring.io/spring-security/reference/servlet/authorization/architecture.html#authz-authorities" rel="noopener noreferrer"&gt;Docs Authorities en Spring&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Con ésto en su lugar, solo nos queda decirle a Spring como descomponer un token JWT y obtener sus authorities a partir de el, otro escenario tan común que ya lo tenemos con mínima configuración.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Configuration
public class JwtAuthConfig {
  @Bean
  JwtAuthenticationConverter jwtAuthenticationConverter(
      JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter) {
    val jwtAuthenticationConverter = new JwtAuthenticationConverter();
    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
    return jwtAuthenticationConverter;
  }

  @Bean
  JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter() {
    val jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
    jwtGrantedAuthoritiesConverter.setAuthorityPrefix("");
    return jwtGrantedAuthoritiesConverter;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bastante autodeclarativo, pero solo le digo que necesito un conversor de JWT a &lt;code&gt;Authentication&lt;/code&gt; y que no le ponga ningún prefijo a las &lt;code&gt;Authorities&lt;/code&gt; que cree a partir del token.&lt;/p&gt;

&lt;p&gt;Con esto tenemos casi todo lo que necesitamos, solo nos queda decirle a Spring que éste endpoint es público.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  @Bean
  SecurityFilterChain securityFilterChain(
      HttpSecurity http, JwtAuthenticationConverter jwtAuthenticationConverter) {
    http.csrf(AbstractHttpConfigurer::disable)
        .sessionManagement(sm -&amp;gt; sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .authorizeHttpRequests(
            authorize -&amp;gt;
                authorize
                    .requestMatchers("/login")
                    .permitAll()
                    .anyRequest()
                    .authenticated())
        .oauth2ResourceServer(
            oauth -&amp;gt; oauth.jwt(jwt -&amp;gt; jwt.jwtAuthenticationConverter(jwtAuthenticationConverter)));
    return http.build();
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Definimos que la &lt;a href="https://docs.spring.io/spring-security/reference/servlet/architecture.html#servlet-securityfilterchain" rel="noopener noreferrer"&gt;SecurityFilterChain&lt;/a&gt; desabilite el CSRF y que usaremos sesiones sin estado. El &lt;code&gt;/login&lt;/code&gt; estará abierto y cualquier otro request necesita estar autenticado. Al final, le indicamos que el server Oauth2 use el &lt;code&gt;JwtAuthenticationConverter&lt;/code&gt; que definimos anteriormente.&lt;/p&gt;

&lt;p&gt;Por supuesto necesitamos testearlo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  @Test
  void mfaTest() {
    MockHttpServletResponse response =
        mockMvcTester
            .post()
            .contentType(MediaType.APPLICATION_FORM_URLENCODED)
            .uri("/login")
            .param("username", "user")
            .param("password", "password")
            .exchange()
            .assertThat()
            .containsHeader(AUTHORIZATION)
            .actual()
            .getResponse();

    var token = requireNonNull(response.getHeader(AUTHORIZATION));
    var decoded = jwtDecoder.decode(token.replace("Bearer ", ""));
    assertThat(decoded.getSubject()).isEqualTo("user");
    assertThat(decoded.getClaimAsStringList(SCOPE)).contains("ROLE_USER");
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aquí me traigo el &lt;code&gt;NimbusJwtDecoder&lt;/code&gt; que utiliza la aplicación para decodificar y valido que el token que me genera el endpoint es válido. Las credenciales son las que definí en mi &lt;code&gt;UserDetailsService&lt;/code&gt; en memoria.&lt;/p&gt;

&lt;p&gt;Con esto en su lugar, puedo habilitar el servicio de &lt;code&gt;/user/settings&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@RestController
@RequestMapping("/user")
public class UserController {

  @GetMapping("/settings")
  ResponseEntity&amp;lt;Map&amp;lt;String, String&amp;gt;&amp;gt; getSettings() {
    return ResponseEntity.ok(Map.of("found", "config"));
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hermosa configuración.&lt;/p&gt;

&lt;p&gt;Y securizarlo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  @Bean
  SecurityFilterChain securityFilterChain(
      HttpSecurity http, JwtAuthenticationConverter jwtAuthenticationConverter) {
    http
    // etc etc
        .authorizeHttpRequests(
            authorize -&amp;gt;
                authorize
                    .requestMatchers("/login")
                    .permitAll()
                    .requestMatchers("/user/settings")
                    .hasRole("USER")
                    .anyRequest()
                    .authenticated())
    // etc etc
    return http.build();
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El &lt;code&gt;JwtAuthenticationConverter&lt;/code&gt; que definí antes va a leer el token y asígnar el Authority con el rol correspondiente.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agregando MFA
&lt;/h3&gt;

&lt;p&gt;Resulta que ahora queremos que, además de que el usuario esté autenticado, valide un código que recibirá por mail o SMS cuando quiera acceder a las settings.&lt;br&gt;
Ahora el requerimiento de seguridad para acceder a las settings es así:&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.amazonaws.com%2Fuploads%2Farticles%2F0ery9enbgyqygbn6i2ko.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%2F0ery9enbgyqygbn6i2ko.png" alt="Diagrama con 2 métodos de autenticación" width="800" height="496"&gt;&lt;/a&gt;&lt;br&gt;
En caso de que alguna condición falle, siempre REJECT.&lt;/p&gt;

&lt;p&gt;Para esto vamos a crear un endpoint donde el cliente puede solicitar un OTT, podríamos dispararlo directamente del endpoint cuando falle, pero sería un acople innecesario.&lt;/p&gt;

&lt;p&gt;Veamos entonces la implementación:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@RestController
@RequestMapping("/ott")
@RequiredArgsConstructor
public class OttController {
  private final JwtEncoder jwtEncoder;
  private final Map&amp;lt;String, OneTimeToken&amp;gt; tokens = new HashMap&amp;lt;&amp;gt;();

  @PostMapping
  @ResponseStatus(HttpStatus.NO_CONTENT)
  void getOttCode() {
    var auth = requireNonNull(SecurityContextHolder.getContext().getAuthentication());
    var ott =
        new DefaultOneTimeToken(
            UUID.randomUUID().toString(), auth.getName(), Instant.now().plusSeconds(1000));

    tokens.put(auth.getName(), ott);
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A efectos de una demo, vamos a guardar los tokens en un mapa, en producción, este storage debería ser un cache distribuido o datastore con TTL.&lt;/p&gt;

&lt;p&gt;Creamos un método publico para validarlo en el test, que lo extendemos así:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    mockMvcTester
        .post()
        .contentType(MediaType.APPLICATION_JSON)
        .uri("/ott")
        .header(AUTHORIZATION, token)
        .exchange()
        .assertThat()
        .hasStatus(HttpStatus.NO_CONTENT);

    assertThat(ottController.getToken("user")).isNotNull();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Muy sencillito.&lt;/p&gt;

&lt;p&gt;El soporte de MFA existe, pero no se activa automáticamente, así que lo habilitamos agregando la anotación &lt;code&gt;@EnableMultiFactorAuthentication(authorities = {})&lt;/code&gt; a nuestra clase de configuracion de Spring Security, authorities vacías ya que especificar aquí los métodos de autenticación los habilitaria globalmente.&lt;/p&gt;

&lt;p&gt;Para obtener el token OTT dijimos que el usuario debería estar autenticado de antemano, como ahora tenemos multiples factores, tenemos que decirle a Spring Security cómo debería estar autenticado para obtenerlo, así que nuestro filter chain quedaría así:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  @Bean
  SecurityFilterChain securityFilterChain(
      HttpSecurity http, JwtAuthenticationConverter jwtAuthenticationConverter) {
    val bearerFactor =
        AuthorizationManagerFactories.multiFactor()
            .requireFactors(FactorGrantedAuthority.BEARER_AUTHORITY)
            .build();
    http
    // etc etc
        .authorizeHttpRequests(
            authorize -&amp;gt;
                authorize
                    .requestMatchers("/ott")
                    .access(bearerFactor.hasRole("USER"))
                    .anyRequest()
                    .authenticated());
    // etc etc
    return http.build();
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;¿Qué está pasando aquí?&lt;/strong&gt;&lt;br&gt;
Ahora necesitamos un manager de autorización que valide que el usuario se autenticó con Bearer Token, JWT.&lt;br&gt;
Y le decimos que para invocar &lt;code&gt;/ott&lt;/code&gt; necesita éste método de autenticación además de tener el rol USER.&lt;br&gt;
Con el setup que tenemos, esto es todo lo que necesitamos, al utilizar un &lt;code&gt;JwtAuthenticationConverter&lt;/code&gt; éste agrega por default &lt;code&gt;FactorGrantedAuthority#BEARER_AUTHORITY&lt;/code&gt; a la autenticación del contexto.&lt;/p&gt;

&lt;p&gt;Entonces ahora solo necesitamos validar el token OTT, en producción éste lo mandaríamos por mail o SMS, aquí, vive en memoría y lo obtengo con un método público en el controlador.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  @PostMapping("/login")
  @ResponseStatus(HttpStatus.NO_CONTENT)
  ResponseEntity&amp;lt;Void&amp;gt; validateOtt(@RequestParam String ott) {
    var auth = requireNonNull(SecurityContextHolder.getContext().getAuthentication());
    var ottToken = tokens.get(auth.getName());
    if (isNull(ottToken) || !ottToken.getTokenValue().equals(ott)) {
      throw new IllegalArgumentException();
    }
    List&amp;lt;String&amp;gt; authorities =
        Stream.of(
                List.of(FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.OTT_AUTHORITY)),
                auth.getAuthorities())
            .flatMap(Collection::stream)
            .map(GrantedAuthority::getAuthority)
            .toList();
    String token =
        jwtEncoder
            .encode(
                JwtEncoderParameters.from(
                    JwsHeader.with(SignatureAlgorithm.RS256).build(),
                    JwtClaimsSet.builder()
                        .subject(auth.getName())
                        .claim(SCOPE, authorities)
                        .expiresAt(Instant.now().plusSeconds(1000))
                        .build()))
            .getTokenValue();
    return ResponseEntity.noContent().header(AUTHORIZATION, "Bearer %s".formatted(token)).build();
  }

  public OneTimeToken getToken(String username) {
    return tokens.get(username);
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El usuario debe pegarle al endpoint con el código que "le llegó por mail" y, si existe, le agregamos a los authorities el &lt;code&gt;FactorGrantedAuthority#OTT_AUTHORITY&lt;/code&gt; indicándo que se autenticó correctamente con éste metodo y creamos un nuevo tóken con toda la información que ya tenía, que luego puede usar para buscar sus settings.&lt;/p&gt;

&lt;p&gt;Lo último sería completar nuestro test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    response =
        mockMvcTester
            .post()
            .uri("/ott/login")
            .queryParam("ott", ott)
            .header(AUTHORIZATION, token)
            .exchange()
            .assertThat()
            .debug()
            .hasStatus(HttpStatus.NO_CONTENT)
            .containsHeader(AUTHORIZATION)
            .extracting(MvcTestResult::getResponse)
            .actual();

    var auth = response.getHeader(AUTHORIZATION);
    assertThat(auth).isNotNull();
    var updatedToken = auth.replace("Bearer ", "");
    assertThatNoException().isThrownBy(() -&amp;gt; jwtDecoder.decode(updatedToken));
    decoded = jwtDecoder.decode(updatedToken);
    assertThat(decoded.getSubject()).isEqualTo("user");
    assertThat(decoded.getClaimAsStringList(SCOPE)).contains("FACTOR_OTT", "FACTOR_BEARER");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Donde testeamos que, con el token que teniamos de la autenticación con contraseña podemos acceder a &lt;code&gt;/ott/login&lt;/code&gt; y obtener el nuevo token con ambos factores de autenticación.&lt;/p&gt;

&lt;p&gt;El soporte nativo de MFA en Spring Security 7 no introduce nuevos mecanismos de verificación por sí mismo, sino algo mucho más importante: un modelo consistente para representarlos y exigirlos.&lt;/p&gt;

&lt;p&gt;En lugar de flujos especiales, estados intermedios o lógica dispersa en filtros y controladores, cada factor de autenticación pasa a formar parte explícita del Authentication del usuario, expresado como autoridades verificables. Esto permite que la autorización se base en hechos comprobables, qué factores fueron validados, y no en suposiciones implícitas o banderas temporales.&lt;/p&gt;

&lt;p&gt;Este enfoque encaja naturalmente con arquitecturas stateless, JWT y APIs REST, y se alinea con cómo los sistemas de identidad modernos modelan la autenticación progresiva o step-up authentication. El resultado es un sistema más simple de razonar, más fácil de mantener y mucho menos propenso a errores sutiles de seguridad.&lt;/p&gt;

&lt;p&gt;Spring Security 7 no hace que MFA sea “mágico”, pero finalmente lo hace predecible, declarativo y bien integrado en su ecosistema. Y eso, para quienes ya tuvieron que implementarlo a mano más de una vez, es un cambio enorme.&lt;/p&gt;

&lt;p&gt;Como siempre, todo el código está disponible para que lo revises en &lt;a href="https://github.com/FdHerrera/spring-mfa-auth-demo" rel="noopener noreferrer"&gt;el proyecto de github&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>security</category>
      <category>java</category>
    </item>
    <item>
      <title>Simplificando Inversion of Control y Dependency Injection</title>
      <dc:creator>Federico Herrera</dc:creator>
      <pubDate>Wed, 10 Dec 2025 14:00:00 +0000</pubDate>
      <link>https://dev.to/fdherrera/simplificando-inversion-of-control-y-dependency-injection-5hkh</link>
      <guid>https://dev.to/fdherrera/simplificando-inversion-of-control-y-dependency-injection-5hkh</guid>
      <description>&lt;p&gt;&lt;strong&gt;Inversión de Control (IoC)&lt;/strong&gt; y &lt;strong&gt;Inyección de Dependencias (DI)&lt;/strong&gt; son dos conceptos que aparecen en prácticamente cualquier entrevista de desarrollo web. Y aun así, muchos desarrolladores con años de experiencia no siempre pueden explicarlos con claridad. No porque no los entiendan, al contrario, sino porque están tan asimilados en el día a día que dejan de ser visibles.&lt;/p&gt;

&lt;p&gt;Un pequeño repaso nunca viene mal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inversion of Control
&lt;/h3&gt;

&lt;p&gt;IoC es un principio fundamental en el que se apoyan todos los frameworks modernos. La idea central es simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Dejar que el Framework lo haga por vos&lt;/em&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Cuando usamos un framework backend como Spring, este toma control de una enorme cantidad de responsabilidades que, sin él, tendrías que escribir a mano:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;manejar el ciclo de vida del servidor,&lt;/li&gt;
&lt;li&gt;gestionar hilos y concurrencia,&lt;/li&gt;
&lt;li&gt;rutear cada request,&lt;/li&gt;
&lt;li&gt;leer y escribir sobre streams I/O,&lt;/li&gt;
&lt;li&gt;serializar y deserializar datos,&lt;/li&gt;
&lt;li&gt;manejar excepciones,&lt;/li&gt;
&lt;li&gt; y proveer herramientas de testing sobre todo eso.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eso es IoC: el control deja de estar en tu código y pasa al framework.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Django (Python)&lt;/li&gt;
&lt;li&gt;Express.js (Node)&lt;/li&gt;
&lt;li&gt;Gin / Gorilla (Go)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Dependency Injection
&lt;/h3&gt;

&lt;p&gt;A diferencia de IoC, DI es un &lt;strong&gt;patrón de diseño&lt;/strong&gt;. Podés usar un framework sin DI, o usar DI sin un framework. Su objetivo es resolver un problema puntual: la provisión de dependencias.&lt;/p&gt;

&lt;p&gt;Cuando una clase necesita algo que está implementado en otra, aparece una dependencia. Por ejemplo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class EmailSender {
    private final EmailWriter emailWriter;

    public EmailSender(EmailWriter emailWriter) {
        this.emailWriter = emailWriter;
    }

    public void sendEmail(EmailContent emailContent) {
        var email = emailWriter.writeEmail(emailContent);
        // etc etc
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En éste ejemplo podemos ver que &lt;code&gt;EmailSender&lt;/code&gt; &lt;strong&gt;depende&lt;/strong&gt; de &lt;code&gt;EmailWriter&lt;/code&gt;, usarlo arrojaría &lt;code&gt;NullPointerException&lt;/code&gt; en caso de que no se proporcione.&lt;/p&gt;

&lt;p&gt;En éste caso si no tuvieramos DI tendríamos 2 soluciones.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Crear la dependencia desde afuera
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class SomeService {
     public void notifySomething(Destiny destiny) {
         var emailWriter = new EmailSender(new EmailWriter());
         EmailContent emailContent = buildEmailContent(destiny);
         emailSender.sendEmail(emailContent);
     }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Crear la dependencia adentro del método
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class EmailSender {

    public void sendEmail(EmailContent emailContent) {
        var emailWriter = new EmailWriter();
        var email = emailWriter.writeEmail(emailContent);
        // etc etc
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ambos enfoques tienen problemas puntuales:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Acople innecesario&lt;/li&gt;
&lt;li&gt;Dificultad para testear&lt;/li&gt;
&lt;li&gt;Imposibilidad de mockear dependencias&lt;/li&gt;
&lt;li&gt;La responsabilidad de crear objetos termina “moviéndose de lugar”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;DI resuelve esto delegando la gestión de dependencias al framework.&lt;/p&gt;

&lt;h3&gt;
  
  
  Demo
&lt;/h3&gt;

&lt;p&gt;Supongamos un API donde los usuarios suben videos. Debemos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Encriptar&lt;/li&gt;
&lt;li&gt;Publicar&lt;/li&gt;
&lt;li&gt;Persistir metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Además:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Soportamos dos tipos de video: MP4 y FLV&lt;/li&gt;
&lt;li&gt;En desarrollo queremos guardar localmente, pero en producción debemos usar un cloud provider&lt;/li&gt;
&lt;li&gt;Queremos poder cambiar de proveedor sin tocar el servicio&lt;/li&gt;
&lt;/ul&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%2F6vfqd2jw12t7rfmymvlz.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%2F6vfqd2jw12t7rfmymvlz.png" alt="Diagrama de flujo de Video Service, request http a POST /api/video hacia un paso de encriptación, luego publicación y finalmente persistencia antes de retornar a cliente" width="800" height="593"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Intentaremos evitar hacer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (isProd()) {
  // usar cloud
} else {
  // guardar en local
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tenemos entonces:&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.amazonaws.com%2Fuploads%2Farticles%2F4uvvni9ym560actz717v.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%2F4uvvni9ym560actz717v.png" alt="Diagrama de flujo del servicio de video, mostrando los pasos de encriptación, publicación y persistencia, con un proceso de bifurcación basado en el ambiente (producción o local), y la leyenda indica los diferentes tipos de bloques." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Aplicando IoC y DI
&lt;/h3&gt;

&lt;p&gt;Para el publisher podemos hacer uso de una interfaz común y profiles, permitiendonos tener 2 implementaciones distintas y solo le indicamos al framework caundo usar cada una y que se encargue del resto.&lt;/p&gt;

&lt;p&gt;La interfaz común:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public interface VideoPublisher {
    URI publishContent(VideoPostRequest videoPostRequest);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Implementación local:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Component
@Profile("local")
public class LocalVideoPublisher implements VideoPublisher {
    private static final Logger log = LoggerFactory.getLogger(LocalVideoPublisher.class);

    @Override
    public URI publishContent(VideoPostRequest videoPostRequest) {
        log.info("We save the video locally");
        return URI.create("file:///local/test/video123");
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Productiva:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Component
@Profile("prod")
public class CloudVideoPublisher implements VideoPublisher {
    private static final Logger log = LoggerFactory.getLogger(CloudVideoPublisher.class);

    @Override
    public URI publishContent(VideoPostRequest videoPostRequest) {
        log.info("Posting to cloud provider!");
        return URI.create("https://mycloudprovider.com/uploads/video123");
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y nuestro service puede simplemente hacer uso de la interfaz, sin involucrarse en detalles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var link = videoPublisher.publishContent(...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DI nos permite testearlo de forma tan simple como dandole un perfil contextual al test y validando que la URI es la esperada:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@SpringBootTest
@ActiveProfiles("local")
class LocalVideoServiceTest {
    @Autowired VideoService videoService;
    @Autowired VideoRepository videoRepository;

    @Test
    void shouldSaveLocally() {
        var input = new VideoRequest(VideoType.MP4, new byte[]{}, "My title", "My description", 1L);
        var result = videoService.postVideo(input);

        var actual = videoRepository.findById(result);

        assertThat(actual).get()
                .extracting(Video::link)
                .isEqualTo(URI.create("file:///local/test/video123"));
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Profile = prod&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@SpringBootTest
@ActiveProfiles("prod")
class ProdVideoServiceTest {
    @Autowired VideoService videoService;
    @Autowired VideoRepository videoRepository;

    @Test
    void shouldSaveOnCloudProvider() {
        var input = new VideoRequest(VideoType.MP4, new byte[]{}, "Some other video", "Significant description", 2L);
        var result = videoService.postVideo(input);

        var actual = videoRepository.findById(result);

        assertThat(actual).get()
                .extracting(Video::link)
                .isEqualTo(URI.create("https://mycloudprovider.com/uploads/video123"));
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>beginners</category>
      <category>java</category>
      <category>programming</category>
      <category>software</category>
    </item>
    <item>
      <title>Spring: la Feature que Tardó 6 Años en Llegar (Y Cómo Cambia Todo)</title>
      <dc:creator>Federico Herrera</dc:creator>
      <pubDate>Fri, 14 Nov 2025 14:00:00 +0000</pubDate>
      <link>https://dev.to/fdherrera/spring-la-feature-que-tardo-6-anos-en-llegar-y-como-cambia-todo-38i0</link>
      <guid>https://dev.to/fdherrera/spring-la-feature-que-tardo-6-anos-en-llegar-y-como-cambia-todo-38i0</guid>
      <description>&lt;p&gt;Recientemente, en un proyecto en el que estoy trabajando, me encontré con un escenario interesante. Estuve un tiempo explorando posibles enfoques hasta que se me ocurrió uno que me pareció bastante elegante.&lt;/p&gt;

&lt;p&gt;Simplificando: recibimos mensajes que, según una combinación de códigos, deben disparar distintas tareas.&lt;/p&gt;

&lt;p&gt;Mi idea fue aplicar un diseño basado en una factory que devuelva una estrategia dependiendo de esa combinación. Gráficamente se vería así:&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.amazonaws.com%2Fuploads%2Farticles%2F78xhvus6s6xtxkm4b3ki.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%2F78xhvus6s6xtxkm4b3ki.png" alt="Diagrama de un mensaje hacia una entidad que consulta a otra por una lista de estrategias" width="789" height="311"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;El service, usando esta petición, consulta a la factory por una estrategia para el caso. La factory, según la combinación de códigos, retorna una estrategia que el service puede ejecutar. El service nunca sabe que estrategia ejecuta — eso lo determina la factory.&lt;/p&gt;

&lt;p&gt;Cada estrategia es &lt;strong&gt;granular&lt;/strong&gt;, &lt;strong&gt;fácil de testear&lt;/strong&gt; y &lt;strong&gt;Open/Closed&lt;/strong&gt;: puedo agregar nuevas estrategias sin modificar las existentes, solo creo una nueva estrategia anotada con &lt;code&gt;@Component&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Vamos al código
&lt;/h3&gt;

&lt;p&gt;Tenemos una interfaz &lt;code&gt;Strategy&lt;/code&gt; muy simple, solo nos dice que combinacion de códigos soporta y expone un método que ejecuta la lógica de negocio para el mensaje dado.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public interface Strategy {
    void consumeMessage(Message message);

    TypeEnum supportsType();

    SubTypeEnum supportsSubType();
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Una implementación típica para la combinacion &lt;code&gt;FOO|BAR&lt;/code&gt;, por ejemplo, se vería así:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
@Component
public class FooBarStrategy implements Strategy {
    private static final Logger log = LoggerFactory.getLogger(FooBarStrategy.class);

    @Override
    public void consumeMessage(Message message) {
        log.info("Hi from FooBar");
    }

    @Override
    public TypeEnum supportsType() {
        return TypeEnum.FOO;
    }

    @Override
    public SubTypeEnum supportsSubType() {
        return SubTypeEnum.BAR;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Los metodos &lt;code&gt;supportsType&lt;/code&gt; y &lt;code&gt;supportsSubType&lt;/code&gt; le dicen la factory para que combinación de códigos está pensado esta estrategia.&lt;/p&gt;

&lt;p&gt;La factory por su lado se vería asi:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Component
public class StrategiesFactory {
    private final Map&amp;lt;Pair&amp;lt;TypeEnum, SubTypeEnum&amp;gt;, Strategy&amp;gt; strategyMap;

    public StrategiesFactory(List&amp;lt;Strategy&amp;gt; strategies) {
        strategyMap = strategies.stream()
                .collect(
                        toMap(strategy -&amp;gt; Pair.of(strategy.supportsType(), strategy.supportsSubType()), Function.identity())
                );
    }

    public Strategy getStrategy(Message message) {
        var strategy = strategyMap.get(Pair.of(message.type(), message.subTypeEnum()));
        if (isNull(strategy)) {
            throw new IllegalArgumentException("Combination not supported");
        }
        return strategy;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spring permite inyectar listas de beans de un mismo tipo.&lt;br&gt;
Así, puedo recibir todas las estrategias registradas sin declararlas una por una. &lt;a href="https://www.baeldung.com/spring-injecting-collections#reference" rel="noopener noreferrer"&gt;Baeldung tiene un gran artículo al respecto!&lt;/a&gt; Pero si quieres que hable sobre esto a detalle en otro post, házmelo saber en los comentarios!&lt;/p&gt;

&lt;p&gt;Para testear la implementación podemos usar un test parametrizado que valide que cada combinación de códigos retorna la estrategia esperada:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@SpringBootTest
class BeansdemoApplicationTests {
    @Autowired StrategiesFactory strategiesFactory;

    static Stream&amp;lt;Arguments&amp;gt; codeCombosAndExpectedStrategy() {
        return Stream.of(
                Arguments.of(TypeEnum.FOO, SubTypeEnum.BAR, FooBarStrategy.class),
                Arguments.of(TypeEnum.FOO, SubTypeEnum.BUZ, FooBuzStrategy.class),
                Arguments.of(TypeEnum.QUX, SubTypeEnum.BAR, QuxBarStrategy.class),
                Arguments.of(TypeEnum.QUX, SubTypeEnum.BUZ, QuxBuzStrategy.class)
        );
    }

    @ParameterizedTest
    @MethodSource("codeCombosAndExpectedStrategy")
    void shouldGetByCodesCombo(TypeEnum typeEnum, SubTypeEnum subTypeEnum, Class&amp;lt;Strategy&amp;gt; expectedStrategyClass) {
        var actualStrategy = strategiesFactory.getStrategy(new Message(typeEnum, subTypeEnum));

        assertThat(actualStrategy).isInstanceOf(expectedStrategyClass);
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  El problema
&lt;/h3&gt;

&lt;p&gt;Todo marchaba bien hasta que me doy con que ciertas combinaciones de códigos compartían la misma lógica: &lt;code&gt;QUX|BAR&lt;/code&gt; y &lt;code&gt;QUX|BUZ&lt;/code&gt; ejecutaban exactamente el mismo flujo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Component
public class QuxBarStrategy implements Strategy {
    private static final Logger log = LoggerFactory.getLogger(QuxBarStrategy.class);

    @Override
    public void consumeMessage(Message message) {
        log.info("Same impl here!");
    }

    @Override
    public TypeEnum supportsType() {
        return TypeEnum.QUX;
    }

    @Override
    public SubTypeEnum supportsSubType() {
        return SubTypeEnum.BAR;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Component
public class QuxBuzStrategy implements Strategy {
    private static final Logger log = LoggerFactory.getLogger(QuxBuzStrategy.class);

    @Override
    public void consumeMessage(Message message) {
        log.info("Same impl here!");
    }

    @Override
    public TypeEnum supportsType() {
        return TypeEnum.QUX;
    }

    @Override
    public SubTypeEnum supportsSubType() {
        return SubTypeEnum.BUZ;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Podemos ver que los tipos que soportan son distintos, pero la implementacion de &lt;code&gt;consumeMessage&lt;/code&gt; es idéntica: &lt;code&gt;log.info("Same impl here!");&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Como ambas estrategias tenían la misma implementación, esto violaba el principio DRY y escalaba mal a medida que aparecían más combinaciones similares.&lt;/p&gt;

&lt;h4&gt;
  
  
  ¿Una solución?
&lt;/h4&gt;

&lt;p&gt;La clave está en el método &lt;code&gt;consumeMessage&lt;/code&gt;: es un &lt;code&gt;Consumer&amp;lt;Message&amp;gt;&lt;/code&gt;.&lt;br&gt;
Si lo pienso así, toda la clase podría reducirse a una estructura de datos que almacene ese consumer junto con los tipos que soporta:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public record DelegateStrategy(
        Consumer&amp;lt;Message&amp;gt; messageConsumer,
        TypeEnum typeSupported,
        SubTypeEnum subTypeEnum
) implements Strategy {
    @Override
    public void consumeMessage(Message message) {
        messageConsumer.accept(message);
    }

    @Override
    public TypeEnum supportsType() {
        return typeSupported();
    }

    @Override
    public SubTypeEnum supportsSubType() {
        return subTypeEnum();
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Con esta abstracción, puedo reutilizar la misma lógica (&lt;code&gt;Consumer&amp;lt;Message&amp;gt;&lt;/code&gt;) para múltiples combinaciones de códigos, eliminando duplicación y manteniendo el diseño extensible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pre Spring Framework 7
&lt;/h3&gt;

&lt;p&gt;En versiones anteriores &lt;strong&gt;no es posible registrar beans de forma dinámica&lt;/strong&gt;, o por lo menos no para mi caso, &lt;code&gt;BeanPostProcessor&lt;/code&gt; o similares son ejecutados demasiado tarde, cuando mi factory ya esta creada, estos se usan principalmente para hacer configuraciones posteriores.&lt;/p&gt;

&lt;p&gt;En 2009 alguien abrió &lt;a href="https://github.com/spring-projects/spring-framework/issues/23471" rel="noopener noreferrer"&gt;este issue&lt;/a&gt; pidiendo exactamente lo que necesito... que fue cerrado como &lt;code&gt;invalid&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;La única alternativa era registrar manualmente cada estrategia, repitiendo código y perdiendo flexibilidad.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ahora, con Spring 7
&lt;/h3&gt;

&lt;p&gt;Con &lt;code&gt;Spring Framework 7&lt;/code&gt; esto cambia. Ahora podemos registrar beans de forma programática usando la nueva API &lt;a href="https://docs.spring.io/spring-framework/reference/7.0/core/beans/java/programmatic-bean-registration.html" rel="noopener noreferrer"&gt;BeanRegistry&lt;/a&gt;, recordemos que (al momento de escribir este post) ésta version esta en construcción, por lo que no es estable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    @Override
    public void register(BeanRegistry registry, Environment env) {
        if (Boolean.parseBoolean(env.getProperty("strategies.registrar"))) {
            Consumer&amp;lt;Message&amp;gt; commonImpl = _ -&amp;gt;  log.info("Same impl here!");
            registry.registerBean("quxBarStrategy", DelegateStrategy.class, spec -&amp;gt; spec.prototype()
                    .lazyInit()
                    .description("QuxBarStrategy bean")
                    .supplier(_ -&amp;gt; new DelegateStrategy(commonImpl, TypeEnum.QUX, SubTypeEnum.BAR)));
            registry.registerBean("quxBuzStrategy", DelegateStrategy.class, spec -&amp;gt; spec.prototype()
                    .lazyInit()
                    .primary()
                    .description("QuxBarStrategy bean")
                    .supplier(_ -&amp;gt; new DelegateStrategy(commonImpl, TypeEnum.QUX, SubTypeEnum.BUZ)));
        }
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esto me permite registrar estrategias dinámicamente —incluso a partir de datos externos— y mantener el sistema abierto a nuevas combinaciones sin tocar la factory.&lt;/p&gt;

&lt;h4&gt;
  
  
  Ultimando Detalles
&lt;/h4&gt;

&lt;p&gt;Como ahora tengo dos formas de registrar las estrategias, cree la property &lt;code&gt;strategies.registrar&lt;/code&gt; para determinar si usar la nueva API o el registro clásico.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@SpringBootTest
class BeansdemoApplicationTests {
    @Autowired StrategiesFactory strategiesFactory;

    static Stream&amp;lt;Arguments&amp;gt; codeCombosAndExpectedStrategy() {
        return Stream.of(
                Arguments.of(TypeEnum.FOO, SubTypeEnum.BAR, FooBarStrategy.class),
                Arguments.of(TypeEnum.FOO, SubTypeEnum.BUZ, FooBuzStrategy.class),
                Arguments.of(TypeEnum.QUX, SubTypeEnum.BAR, QuxBarStrategy.class),
                Arguments.of(TypeEnum.QUX, SubTypeEnum.BUZ, QuxBuzStrategy.class)
        );
    }

    @ParameterizedTest
    @DisabledIf(expression = "#{'${strategies.registrar}' == 'true'}", loadContext = true)
    @MethodSource("codeCombosAndExpectedStrategy")
    void shouldGetByCodesCombo(TypeEnum typeEnum, SubTypeEnum subTypeEnum, Class&amp;lt;Strategy&amp;gt; expectedStrategyClass) {
        var actualStrategy = strategiesFactory.getStrategy(new Message(typeEnum, subTypeEnum));

        assertThat(actualStrategy).isInstanceOf(expectedStrategyClass);
    }

    static Stream&amp;lt;Arguments&amp;gt; codeCombosAndExpectedStrategyWhenRegistrar() {
        return Stream.of(
                Arguments.of(TypeEnum.FOO, SubTypeEnum.BAR, FooBarStrategy.class),
                Arguments.of(TypeEnum.FOO, SubTypeEnum.BUZ, FooBuzStrategy.class),
                Arguments.of(TypeEnum.QUX, SubTypeEnum.BAR, DelegateStrategy.class),
                Arguments.of(TypeEnum.QUX, SubTypeEnum.BUZ, DelegateStrategy.class)
        );
    }

    @ParameterizedTest
    @DisabledIf(expression = "#{'${strategies.registrar}' == 'false'}", loadContext = true)
    @MethodSource("codeCombosAndExpectedStrategyWhenRegistrar")
    void shouldGetByCodesCombo_registrar(TypeEnum typeEnum, SubTypeEnum subTypeEnum, Class&amp;lt;Strategy&amp;gt; expectedStrategyClass) {
        var actualStrategy = strategiesFactory.getStrategy(new Message(typeEnum, subTypeEnum));

        assertThat(actualStrategy).isInstanceOf(expectedStrategyClass);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;De esta manera, si el registrar está habilitado, verifico que las combinaciones &lt;code&gt;QUX|BAR&lt;/code&gt; y &lt;code&gt;QUX|BUZ&lt;/code&gt; sean instancias de DelegateStrategy. Si no, pruebo las clases concretas como antes.&lt;/p&gt;

&lt;p&gt;Este patrón de estrategias con factory me permitió mantener un diseño limpio, flexible y fácil de extender.&lt;br&gt;
Con las nuevas capacidades de registro programático en Spring 7, se abre una puerta interesante para generar beans dinámicos y reducir duplicación de código sin perder claridad.&lt;/p&gt;

&lt;p&gt;Todo el código mostrado aquí esta disponible en &lt;a href="https://github.com/FdHerrera/beansdemo" rel="noopener noreferrer"&gt;éste proyecto&lt;/a&gt; en mi Github 🚀🚀&lt;/p&gt;

&lt;p&gt;Te invito a contarme que opinas, como lo habrías hecho vos o si queres que profundice en alguno de los puntos mencionados en los comentarios! 👇&lt;/p&gt;

</description>
      <category>spring</category>
      <category>springboot</category>
      <category>java</category>
      <category>programming</category>
    </item>
    <item>
      <title>Por qué empecé un blog?</title>
      <dc:creator>Federico Herrera</dc:creator>
      <pubDate>Wed, 12 Nov 2025 11:52:32 +0000</pubDate>
      <link>https://dev.to/fdherrera/por-que-empece-un-blog-4i80</link>
      <guid>https://dev.to/fdherrera/por-que-empece-un-blog-4i80</guid>
      <description>&lt;h2&gt;
  
  
  Que tal! Soy Fede Herrera 👋
&lt;/h2&gt;

&lt;p&gt;Un desarrollador Java con varios años de experiencia en la industria. He trabajado para empresas gigantes, como &lt;strong&gt;son Globant, EY, Disney Media y BBVA Argentina&lt;/strong&gt;. Solo para dar un poco de contexto sobre quién soy antes de empezar.&lt;/p&gt;

&lt;p&gt;Hay varios motivos por los que me interesa escribir. El principal, y probablemente el que más se repite entre quienes deciden exponer sus ideas, es &lt;strong&gt;crear una marca&lt;/strong&gt;. Ganar alcance, interacciones y tráfico online es, sin duda y sin descubrir nada nuevo, una de las formas más fáciles de construir conexiones.&lt;/p&gt;

&lt;p&gt;Una segunda razón importante es que &lt;strong&gt;hay poco contenido técnico en español&lt;/strong&gt;. Tengo bastante experiencia en el área y, aunque mi nivel de inglés es alto desde antes de empezar mi carrera, siempre fue mucho más fácil encontrar buenos posts o guías si buscabas directamente en inglés —al menos antes de la explosión de la IA.&lt;/p&gt;

&lt;p&gt;En tercer lugar, &lt;strong&gt;no creo que hacer TikToks sea para mí&lt;/strong&gt; (al menos por ahora). Se me cruzó la idea cuando pensaba cómo empezar, inspirada en un video que se le hizo viral a mi hermano y lo motivó a crear más contenido. No lo descarto a futuro: es una plataforma enorme y, aunque no sea la primera opción educativa de nadie, un video corto y bien hecho siempre atrapa.&lt;/p&gt;

&lt;p&gt;En fin, este es &lt;strong&gt;mi primer post&lt;/strong&gt; para dar una especie de &lt;em&gt;kickoff&lt;/em&gt; a mi aventura en esta plataforma.&lt;/p&gt;

&lt;p&gt;Mi idea es compartir contenido principalmente sobre &lt;strong&gt;Java, Spring Framework y Cloud&lt;/strong&gt;, tal vez algo de &lt;strong&gt;DevOps&lt;/strong&gt; que estoy empezando a aprender y &lt;strong&gt;Kubernetes&lt;/strong&gt;. Soy un apasionado del testing, así que no se sorprendan de ver algún post sobre &lt;strong&gt;testing unitario y de integración, TDD, Mockito, WireMock, MockServer y Testcontainers.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Si leíste hasta aquí, ¡muchas gracias! 🙌&lt;br&gt;
Es una lectura corta, pero para mí significa mucho que te hayas quedado.&lt;br&gt;
Si te interesa, puedes conectar conmigo en &lt;a href="https://www.linkedin.com/in/federicoherreradev/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; y &lt;a href="https://github.com/FdHerrera" rel="noopener noreferrer"&gt;GitHub.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;¡Espero leernos pronto! 👋&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>career</category>
      <category>contentwriting</category>
    </item>
  </channel>
</rss>
