Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# IDE
.vscode/
.idea/
*.swp
*.swo
*~

# Java/Maven
target/
*.class
*.jar
*.war
*.ear
.classpath
.project
.settings/

# Build
.DS_Store
*.log

# Local environment
.java-version
.env
.env.local

# Test/validation scripts
VALIDAR_PRUEBA.sh
120 changes: 120 additions & 0 deletions REQUIREMENTS_VALIDATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Validación de Requerimientos - Similar Products API

## ✅ Requerimientos Cumplidos

| Requerimiento | Cumplido | Detalles |
|---------------|----------|----------|
| **API REST Spring Boot** | ✅ | `GET /product/{productId}/similar` en puerto 5000 |
| **Integración APIs existentes** | ✅ | Consume `/similarids` y `/product/{id}` desde mocks |
| **Performance** | ✅ | Paralelización con ExecutorService (CompletableFuture) |
| **Resilience** | ✅ | Timeouts por producto + manejo de errores completo |
| **Code Clarity** | ✅ | Arquitectura MVC, documentación, constantes nombradas |

---

## 🏗️ Arquitectura

```
GET /product/{productId}/similar (Puerto 5000)
SimilarProductsController (validación)
SimilarProductsService (orquestación)
↓ [En Paralelo - ExecutorService]
├→ getSimilarProductIds() → IDs
├→ getProductDetail() × N (paralelo)
└→ getProductDetail() × N (paralelo)
Retornar List<ProductDetail>
```

**Capas:**
- **Controller**: HTTP endpoint + validación
- **Service**: Orquestación paralela
- **ExternalApiService**: Integración con APIs externas
- **Model**: DTO ProductDetail

---

## 🚀 Performance: Paralelización

**Problema:** 5 productos × 10 seg/producto = 50 segundos

**Solución - Paralelo con CompletableFuture:**
```java
List<CompletableFuture<ProductDetail>> futures = new ArrayList<>();
for (String id : similarIds) {
futures.add(CompletableFuture.supplyAsync(
() -> externalApiService.getProductDetail(id),
executorService // Thread pool de 10
));
}
```

**Resultado:** 5 productos en ~10 segundos (máximo del grupo)
**Mejora:** **5x más rápido**

---

## 🛡️ Resilience: Tolerancia a Fallos

✅ **Timeout por producto:** 15 segundos max
✅ **Si uno falla, otros continúan:** Retorna resultados parciales
✅ **Timeout HTTP:** Connect 5s + Read 10s
✅ **Never crashes:** Nunca retorna 500 Error
✅ **Validación input:** productId no vacío
✅ **Logging:** Todos los errores se registran en logs para debugging

**Ejemplo:**
- Productos: 5
- Uno tarda 20 seg (timeout 15s)
- Retorna: 4 productos (no falla)
- Log: WARN/ERROR en logs con detalles para debugging

---

## 📂 Estructura del Código

```
app/src/main/java/com/similarproducts/
├── Application.java
├── controller/ → SimilarProductsController
├── service/ → SimilarProductsService, ExternalApiService
└── model/ → ProductDetail

app/src/main/resources/
└── application.yml (puerto 5000)
```

---

## 🧪 Ejemplo de Uso

```bash
# Request
GET http://localhost:5000/product/1/similar

# Response (200 OK)
[
{
"id": "2",
"name": "Similar Product",
"price": 29.99,
"availability": true
}
]

# Si no hay similares (200 OK - nunca falla)
[]
```

---

## 🔧 Configuración

- **Puerto:** 5000 ✅
- **Framework:** Spring Boot 3.3.8
- **Java:** 21 LTS
- **Build:** Maven
- **Thread Pool:** 10 threads
- **Timeouts:** Configurables
68 changes: 68 additions & 0 deletions app/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.8</version>
<relativePath/>
</parent>

<groupId>com.similarproducts</groupId>
<artifactId>app</artifactId>
<version>1.0-SNAPSHOT</version>

<name>app</name>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>

<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- RestClient for HTTP calls -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

<!-- Lombok for boilerplate reduction -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<!-- Logging with Lombok -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>

<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
12 changes: 12 additions & 0 deletions app/src/main/java/com/similarproducts/Application.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.similarproducts;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.similarproducts.controller;

import com.similarproducts.model.ProductDetail;
import com.similarproducts.service.SimilarProductsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("/product")
public class SimilarProductsController {

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

private final SimilarProductsService similarProductsService;

public SimilarProductsController(SimilarProductsService similarProductsService) {
this.similarProductsService = similarProductsService;
}

@GetMapping("/{productId}/similar")
public ResponseEntity<List<ProductDetail>> getSimilarProducts(
@PathVariable String productId) {
try {
if (productId == null || productId.trim().isEmpty()) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ArrayList<>());
}

List<ProductDetail> similarProducts = similarProductsService.getSimilarProducts(productId);

return ResponseEntity.ok(similarProducts);

} catch (Exception e) {
logger.error("Unexpected error while fetching similar products for productId: {}. Error: {}", productId,
e.getMessage(), e);
return ResponseEntity.ok(new ArrayList<>());
}
}
}
24 changes: 24 additions & 0 deletions app/src/main/java/com/similarproducts/model/ProductDetail.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.similarproducts.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.fasterxml.jackson.annotation.JsonProperty;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ProductDetail {

@JsonProperty("id")
private String id;

@JsonProperty("name")
private String name;

@JsonProperty("price")
private Double price;

@JsonProperty("availability")
private Boolean availability;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.similarproducts.service;

import com.similarproducts.model.ProductDetail;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.concurrent.TimeUnit;

@Service
public class ExternalApiService {

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

private final RestTemplate restTemplate;
private final String mockBaseUrl;

private static final int CONNECT_TIMEOUT_SECONDS = 5;
private static final int READ_TIMEOUT_SECONDS = 10;

public ExternalApiService(
@Value("${external.api.mock.url:http://localhost:3001}") String mockBaseUrl) {
this.mockBaseUrl = mockBaseUrl;
this.restTemplate = createRestTemplate();
}

public String[] getSimilarProductIds(String productId) {
String url = mockBaseUrl + "/product/" + productId + "/similarids";
try {
String[] ids = restTemplate.getForObject(url, String[].class);
return ids != null ? ids : new String[0];
} catch (Exception e) {
logger.warn("Failed to fetch similar product IDs for productId: {}. Error: {}", productId, e.getMessage());
return new String[0];
}
}

public ProductDetail getProductDetail(String productId) {
String url = mockBaseUrl + "/product/" + productId;
try {
ProductDetail detail = restTemplate.getForObject(url, ProductDetail.class);
return detail;
} catch (Exception e) {
logger.warn("Failed to fetch product details for productId: {}. Error: {}", productId, e.getMessage());
return null;
}
}

private RestTemplate createRestTemplate() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout((int) TimeUnit.SECONDS.toMillis(CONNECT_TIMEOUT_SECONDS));
factory.setReadTimeout((int) TimeUnit.SECONDS.toMillis(READ_TIMEOUT_SECONDS));
return new RestTemplate(factory);
}
}
Loading