API Rest Spring Boot : Secuencia de Fibonacci

Muchas veces las pruebas técnicas parten de problemas simples:

Hace algún tiempo, en un proceso de selección se me planteó como prueba técnica el desarrollo de un API REST en Java para calcular el n-ésimo número de la secuencia de Fibonacci.

El problema a resolver requería aplicar una serie de conceptos básicos que quizá les pueda resultar útil si recién están empezando con APIs y Spring Boot.

Los requerimientos planteados eran los siguientes:

1- Realizar una API Rest en java (se puede utilizar algun framework como spring o quarkus) que implemente un algoritmo que devuelva el n-esimo numero de Fibonacci.

2- Guardar los resultados intermedios en una base de datos relacional con algún ORM y ser utilizada esta como cache para mejorar los tiempos de la función.

3- Realizar pruebas automatizadas con 80% de cubrimiento de código.

4- Guardar una estadística en la base de datos con respecto a cuales fueron los números que se consultaron más. (Opcional 1)

5- Publicar la solución en alguna nube. (Opcional 2)

Esta solución implementada contempla todos los puntos solicitados, incluidos los opcionales.

https://github.com/nbent1996/apifibonacci

El valor de este ejercicio estaba en demostrar los siguientes puntos:

  • Diseñar un endpoint REST claro
  • Separar responsabilidad entre controller, service y repository.
  • Evitar recalcular valores ya conocidos.
  • Persistir datos usando un ORM (en este caso Hibernate)
  • Registrar estadisticas de uso
  • Cubrir un 80% de la logica con pruebas automatizadas (Jacoco)
  • Finalmente publicar la solución para que pueda ser consumida en una nube (Este punto lo veremos en un próximo post).

Para generar el esqueleto inicial del proyecto se utilizo la pagina https://start.spring.io/ , que nos permite detallarle en que lenguaje vamos a trabajar, con que herramienta utilizaremos la gestión de dependencias (en este caso Maven), la versión de Spring boot, las dependencias y otras configuraciones pertinentes.

Diseño general de la solución

Luego de generar el proyecto base, la aplicación se organizó siguiendo una estructura por capas:

  • Controllers
  • Models
  • Repositories
  • Services
  • DTO

Controladores

Los controladores se encargan de exponer los endpoint rest desarrollados, son el punto de partida, en el caso de este prototipo tenemos 2 controladores:

ResultadoController

@RestController
@RequestMapping("/resultado")
public class ResultadoController {
    @Autowired
    ResultadoService resultadoService;

    @GetMapping("/obtenerTodos")
    public List<ResultadosDTO> obtenerResultados(){
        return resultadoService.obtenerResultados();
    }

    @GetMapping("/limpiarCache")
    public void limpiarCache(){
        resultadoService.limpiarCache();
    }

    @GetMapping("/getFibonacci/{n}")
    public ResponseEntity<ResultadosDTO> getFibonacci(@PathVariable Long n) throws Exception {
        return resultadoService.getFibonacciValue(n);
    }
    
}


IndicadoresController

@RestController
@RequestMapping("/indicador")
public class IndicadoresController {
    @Autowired
    IndicadoresService indicadoresService;

    @GetMapping("/obtenerTodos")
    public List<IndicadoresDTO> obtenerIndicadores(){
        return indicadoresService.obtenerIndicadores();
    }

    @GetMapping("/getIndicador/{id}")
    public ResponseEntity<IndicadoresDTO> getIndicador(@PathVariable Long id) throws Exception{
        return this.indicadoresService.obtenerIndicadorPorPositionResultado(id);
    }

    @GetMapping("/mejoresIndicadores")
    public List<IndicadoresDTO> TopRequestByOrderDesc(){
        return indicadoresService.TopRequestByOrderDesc();
    }
}

Models

En este package tenemos todos los objetos que representan una entidad de nuestro ORM (hibernate), cada entidad representa una tabla y los atributos tienen una serie de configuraciones, acorde a como estructuramos la base de datos.

ResultadoModel

@Entity
@Table(name="fib_resultados")
public class ResultadoModel {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "position", nullable = false, unique = true)
    private Long position;

    @Column(name = "fibonacci_value", nullable = false)
    private Long fibonacci_value;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getFibonacci_value() {
        return fibonacci_value;
    }

    public void setFibonacci_value(Long fibonacci_value) {
        this.fibonacci_value = fibonacci_value;
    }

    public Long getPosition() {
        return position;
    }

    public void setPosition(Long position) {
        this.position = position;
    }

    public ResultadoModel(Long position, Long fibonacci_value) {
        this.position = position;
        this.fibonacci_value = fibonacci_value;
    }

    public ResultadoModel(Long id, Long position, Long fibonacci_value) {
        this.id = id;
        this.position = position;
        this.fibonacci_value = fibonacci_value;
    }

    public ResultadoModel() {
    }
     
}

IndicadoresModel

@Entity
@Table(name="fib_indicadores")
public class IndicadoresModel {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "resultado_id", referencedColumnName = "id")
    private ResultadoModel resultado;

    private Long requestCount=1L;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public ResultadoModel getResultado() {
        return resultado;
    }

    public void setResultado(ResultadoModel resultado) {
        this.resultado = resultado;
    }

    public Long getRequestCount() {
        return requestCount;
    }

    public void setRequestCount(Long requestCount) {
        this.requestCount = requestCount;
    }

    public IndicadoresModel(Long id, ResultadoModel resultado, Long requestCount) {
        this.id = id;
        this.resultado = resultado;
        this.requestCount = requestCount;
    }

    public IndicadoresModel(ResultadoModel resultado, Long requestCount) {
        this.resultado = resultado;
        this.requestCount = requestCount;
    }

    public IndicadoresModel() {
    }
    
}

Services

Este conjunto de clases concentra la lógica central del proyecto, tomando de los repository la data de la bd, y brindándosela a los controller.

IndicadoresService

@Service
public class IndicadoresService {

    private static final Logger logger = LoggerFactory.getLogger(IndicadoresService.class);

    @Autowired
    IndicadoresRepository indicadoresRepository;
    
    public List<IndicadoresDTO> obtenerIndicadores(){
        return mapToIndicadoresDTOList(indicadoresRepository.findAll());
    }

    public List<IndicadoresDTO> TopRequestByOrderDesc(){
        return mapToIndicadoresDTOList(indicadoresRepository.findTopRequestByOrderDesc());
    }

    public void deleteAll(){
        indicadoresRepository.deleteAll();
    }

    public IndicadoresDTO guardarIndicador(IndicadoresModel indicador){
        return mapToIndicadoresDTO(indicadoresRepository.save(indicador));
    }

    public ResponseEntity<IndicadoresDTO> obtenerIndicadorPorPositionResultado(@Param("resultadoPosition") Long position) throws Exception {
        IndicadoresModel indicador = indicadoresRepository.getIndicadorByResultadoPosition(position)
                .orElseThrow(() -> new Exception("No existe indicador para la posición: " + position));

        return ResponseEntity.ok(mapToIndicadoresDTO(indicador));
    }
 
    public ResponseEntity<IndicadoresDTO> aumentarIndicador(ResultadoModel resultado) throws Exception {
        try{
        Optional<IndicadoresModel> indicadorExistente = indicadoresRepository.getIndicadorByResultadoPosition(resultado.getPosition());
        if(indicadorExistente.isPresent()){
            indicadorExistente.get().setRequestCount(indicadorExistente.get().getRequestCount()+1L);
            indicadoresRepository.save(indicadorExistente.get());
            return ResponseEntity.ok(mapToIndicadoresDTO(indicadorExistente.get()));
        }else{
            IndicadoresModel ind = new IndicadoresModel();
            ind.setResultado(resultado);
            ind.setRequestCount(1L);
            return ResponseEntity.ok(mapToIndicadoresDTO(indicadoresRepository.save(ind)));
        }
    }catch(Exception ex){
        logger.error("Error al obtener el indicador por ID de resultado: " + ex.getMessage(), ex);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
    }
    
    }
    private List<IndicadoresDTO> mapToIndicadoresDTOList(Iterable<IndicadoresModel> iterable) {
        List<IndicadoresDTO> retorno = new ArrayList<>();

        for (IndicadoresModel item : iterable) {
            retorno.add(mapToIndicadoresDTO(item));
        }

        return retorno;
    }
    private IndicadoresDTO mapToIndicadoresDTO(IndicadoresModel model) {
        if (model == null) {
            return null;
        }

        ResultadoModel resultado = model.getResultado();

        return new IndicadoresDTO(
                model.getId(),
                resultado != null ? resultado.getId() : null,
                resultado != null ? resultado.getPosition() : null,
                resultado != null ? resultado.getFibonacci_value() : null,
                model.getRequestCount()
        );
    }
}

ResultadoService

@Service
public class ResultadoService  {

    private static final Logger logger = LoggerFactory.getLogger(ResultadoService.class);

    @Autowired
    ResultadoRepository resultadoRepository;

    @Autowired
    IndicadoresService indicadoresService;

    public List<ResultadosDTO> obtenerResultados(){
        return mapToResultadoDTOList(resultadoRepository.findAll());
    }

    public void limpiarCache(){
        indicadoresService.deleteAll();
        resultadoRepository.deleteAll();
    }

    public ResultadoModel guardarResultado(ResultadoModel resultado){
        return resultadoRepository.save(resultado);
    }

    public ResponseEntity<ResultadosDTO> getFibonacciValue(Long n) {

        if (n == null || n <= 0) {
            return ResponseEntity.badRequest().build();
        }

        try {
            Optional<ResultadoModel> resultadoExistente = resultadoRepository.findByPosition(n);

            if (resultadoExistente.isPresent()) {
                ResultadoModel resultado = resultadoExistente.get();

                indicadoresService.aumentarIndicador(resultado);

                ResultadosDTO dto = mapToResultadoDTO(resultado);

                return ResponseEntity.ok(dto);
            }

            Optional<ResultadosDTO> resultadoNuevo = calcularFibonacci(n);

            if (resultadoNuevo.isEmpty()) {
                return ResponseEntity.internalServerError().build();
            }

            return ResponseEntity.ok(resultadoNuevo.get());

        } catch (Exception ex) {
            logger.error("Error al calcular el número Fibonacci para la posición {}", n, ex);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

    public Optional<ResultadosDTO> calcularFibonacci(Long n) throws Exception {
        if(n<=0){
            throw new Exception("El indice debe ser un numero positivo.");
        }
        //Si existe el valor lo devuelvo
        Optional<ResultadosDTO> cachedResult = resultadoRepository.findByPosition(n)
                .map(this::mapToResultadoDTO);
        if (cachedResult.isPresent()) {
            return cachedResult;
        }

        if (n == 1) {
            return persistirResultado(n, 1L, 1)
                    .map(this::mapToResultadoDTO);
        }

        if (n == 2) {
            return persistirResultado(n, 2L, 2)
                    .map(this::mapToResultadoDTO);
        }

        //Valores por defecto, posiciones iniciales
        Long fib = 2L;
        Long prevFib=1L;
        int start = 3;

       // Encuentra el máximo almacenado por debajo de `n`
    Optional<ResultadoModel> lastCachedResult = resultadoRepository.findMaxPositionWithLimit(n);
    if (lastCachedResult.isPresent()) {
        ResultadoModel lastCached = lastCachedResult.get();
        // Corregir cálculo de prevFib
        if (lastCached.getPosition() > 1) {
            Optional<ResultadoModel> previous = resultadoRepository.findByPosition(lastCached.getPosition() - 1);
            if (previous.isPresent()) {
                prevFib = previous.get().getFibonacci_value();
            } else {
                prevFib = lastCached.getFibonacci_value() - fib; 
            }
        }
        fib = lastCached.getFibonacci_value();
        start = lastCached.getPosition().intValue() + 1; 
    }

    Optional<ResultadoModel> retorno = Optional.empty();
    // Continuamos la secuencia
    for (int i = start; i <= n; i++) {
        Long newFib = fib + prevFib; // Calcular F(n+1) = F(n) + F(n-1)
        logger.info("Iteracion i=" + i + " // " + "prevFib=" + prevFib + " // " + "fib=" + fib + " // " + "newFib: " + newFib + " // n=" + n);
        prevFib = fib; 
        fib = newFib;
        // Persistimos valor intermedio actualizado
        retorno = persistirResultado(i+0L, fib, i);
    }
    logger.info("-----------------------------------------------------------------------------");
        return retorno.map(this::mapToResultadoDTO);
    }
    public Optional<ResultadoModel> persistirResultado(Long n, Long fib, int position) throws Exception{
            /*Persistimos valor intermedio*/
            Optional<ResultadoModel> resultadoExistente = resultadoRepository.findByPosition(position+0L);
            if(!resultadoExistente.isPresent()){
                ResultadoModel resultadoNuevo = new ResultadoModel();
                resultadoNuevo.setFibonacci_value(fib);
                resultadoNuevo.setPosition(position+0L);
                resultadoNuevo= resultadoRepository.save(resultadoNuevo);
                indicadoresService.aumentarIndicador(resultadoNuevo);
                return Optional.of(resultadoNuevo);
            }else{
            return resultadoExistente;
        }
}
    private List<ResultadosDTO> mapToResultadoDTOList(Iterable<ResultadoModel> iterable) {
        List<ResultadosDTO> retorno = new ArrayList<>();

        for (ResultadoModel item : iterable) {
            retorno.add(mapToResultadoDTO(item));
        }

        return retorno;
    }
    private ResultadosDTO mapToResultadoDTO(ResultadoModel model) {
        if (model == null) {
            return null;
        }

        return new ResultadosDTO(
                model.getId(),
                model.getPosition(),
                model.getFibonacci_value()
        );
    }
}

Repositories

Las clases Repository contienen toda la lógica de acceso a datos, la implementación puede ser mediante queries SQL nativas, o por medio de queries con JPA.

En el caso de este prototipo, se utilizo la interfaz CrudRepository de la dependencia Spring Data, que por si misma nos brinda operaciones básicas con base de datos, sin tener que implementar código adicional.

IndicadoresRepository

@Repository
public interface IndicadoresRepository extends CrudRepository<IndicadoresModel, Long> {

    @Query("SELECT i FROM IndicadoresModel i ORDER BY i.requestCount DESC LIMIT 5")
    ArrayList<IndicadoresModel> findTopRequestByOrderDesc();

    @Query("SELECT i FROM IndicadoresModel i WHERE i.resultado.position = :resultadoPosition")
    Optional<IndicadoresModel> getIndicadorByResultadoPosition(@Param("resultadoPosition")Long resultadoPosition);

    @Query(value = "SELECT * FROM IndicadoresModel ORDER BY id ASC LIMIT 50", nativeQuery = true)
    ArrayList<IndicadoresModel> find50Indicadores();
}

ResultadoRepository

@Repository
public interface ResultadoRepository extends CrudRepository<ResultadoModel, Long> {

    @Query("SELECT r FROM ResultadoModel r WHERE r.position = :position")
    Optional<ResultadoModel> findByPosition(Long position);

    @Query("SELECT r FROM ResultadoModel r ORDER BY r.position DESC LIMIT 1")
    Optional<ResultadoModel> findMaxPosition();

    @Query("SELECT r FROM ResultadoModel r WHERE r.position <= :maxPosition ORDER BY r.position DESC LIMIT 1")
    Optional<ResultadoModel> findMaxPositionWithLimit(Long maxPosition);

    @Query(value = "SELECT * FROM ResultadoModel ORDER BY id ASC LIMIT 50", nativeQuery = true)
    ArrayList<ResultadoModel> find50Resultado();

}

DTO

Este package contiene los DTO (Data Transfer Object) utilizados para transportar la información desde las clases Service hasta ser expuestas en la API, la idea es que el DTO represente como queremos exponer los datos hacia afuera, mientras que los objetos Entity reflejen como se guardan los datos en la base, y que si dichos objetos entity deben ser modificados no tengan incidencia en la API.

Pruebas unitarias

En lo que respecta al requerimiento de las pruebas unitarias, se hizo la implementación correspondiente en este proyecto, si bien no se utilizó Sonarqube, se utilizó el plugin de Maven JaCoCo, el cual nos sirve para generar un reporte que nos indica el porcentaje de cobertura en el código con los test cases.

Tengo un post que les puede ser de utilidad para abordar este tema: https://filenotfound.com.uy/2025/06/gestion-de-calidad-de-codigo-con-sonarqube-jacoco-y-junit/

Estadísticas

En este caso, si bien no se especificó que estadística se requería, la que yo implemente fue el top 5 de posiciones de la secuencia de Fibonacci más solicitadas al api.

Leave a Reply

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *