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
115 changes: 115 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?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.2.0</version>
<relativePath/>
</parent>

<groupId>com.zara</groupId>
<artifactId>similar-products-api</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>

<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<testcontainers.version>1.19.3</testcontainers.version>
<archunit.version>1.2.1</archunit.version>
</properties>

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

<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mockserver</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>${archunit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<includes>
<include>**/*Test.java</include>
<include>**/*Tests.java</include>
</includes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<includes>
<include>**/*IT.java</include>
<include>**/*IntegrationTest.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.zara.similarproducts;

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

@SpringBootApplication
public class SimilarProductsApplication {

public static void main(String[] args) {
SpringApplication.run(SimilarProductsApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.zara.similarproducts.application.port.in;

import com.zara.similarproducts.domain.model.ProductId;
import com.zara.similarproducts.domain.model.SimilarProducts;
import reactor.core.publisher.Mono;

public interface GetSimilarProductsUseCase {

Mono<SimilarProducts> execute(ProductId productId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.zara.similarproducts.application.port.out;

import com.zara.similarproducts.domain.model.Product;
import com.zara.similarproducts.domain.model.ProductId;
import reactor.core.publisher.Mono;

public interface ProductRepository {

Mono<Product> findById(ProductId productId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.zara.similarproducts.application.port.out;

import com.zara.similarproducts.domain.model.ProductId;
import reactor.core.publisher.Flux;

public interface SimilarProductsRepository {

Flux<ProductId> findSimilarProductIds(ProductId productId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.zara.similarproducts.application.usecase;

import com.zara.similarproducts.application.port.in.GetSimilarProductsUseCase;
import com.zara.similarproducts.domain.model.ProductId;
import com.zara.similarproducts.domain.model.SimilarProducts;
import com.zara.similarproducts.domain.service.SimilarProductsDomainService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

import java.time.Duration;

@Service
public class GetSimilarProductsUseCaseImpl implements GetSimilarProductsUseCase {

private static final Logger logger = LoggerFactory.getLogger(GetSimilarProductsUseCaseImpl.class);
private static final Duration TIMEOUT = Duration.ofSeconds(10);

private final SimilarProductsDomainService domainService;

public GetSimilarProductsUseCaseImpl(SimilarProductsDomainService domainService) {
this.domainService = domainService;
}

@Override
public Mono<SimilarProducts> execute(ProductId productId) {
logger.info("Executing GetSimilarProducts use case for product: {}", productId.value());

return domainService.findSimilarProducts(productId)
.timeout(TIMEOUT)
.doOnSuccess(result -> logger.info("Found {} similar products for product: {}",
result.size(), productId.value()))
.doOnError(error -> logger.error("Error finding similar products for product: {}",
productId.value(), error));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.zara.similarproducts.domain.model;

public abstract class DomainException extends RuntimeException {

protected DomainException(String message) {
super(message);
}

protected DomainException(String message, Throwable cause) {
super(message, cause);
}

public abstract String getErrorCode();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.zara.similarproducts.domain.model;

public class ExternalServiceException extends DomainException {

private static final String ERROR_CODE = "EXTERNAL_SERVICE_ERROR";

public ExternalServiceException(String message) {
super(message);
}

public ExternalServiceException(String message, Throwable cause) {
super(message, cause);
}

@Override
public String getErrorCode() {
return ERROR_CODE;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.zara.similarproducts.domain.model;

public class InvalidProductIdException extends DomainException {

private static final String ERROR_CODE = "INVALID_PRODUCT_ID";
private final String productId;

public InvalidProductIdException(String productId) {
super("Invalid product ID format: " + productId);
this.productId = productId;
}

public String getProductId() {
return productId;
}

@Override
public String getErrorCode() {
return ERROR_CODE;
}
}
29 changes: 29 additions & 0 deletions src/main/java/com/zara/similarproducts/domain/model/Product.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.zara.similarproducts.domain.model;

import java.math.BigDecimal;
import java.util.Objects;

public record Product(
ProductId id,
String name,
BigDecimal price,
boolean availability
) {

public Product {
Objects.requireNonNull(id, "Product ID cannot be null");
Objects.requireNonNull(name, "Product name cannot be null");
Objects.requireNonNull(price, "Product price cannot be null");

if (name.isBlank()) {
throw new IllegalArgumentException("Product name cannot be blank");
}
if (price.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Product price cannot be negative");
}
}

public static Product of(ProductId id, String name, BigDecimal price, boolean availability) {
return new Product(id, name, price, availability);
}
}
20 changes: 20 additions & 0 deletions src/main/java/com/zara/similarproducts/domain/model/ProductId.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.zara.similarproducts.domain.model;

import java.util.Objects;

public record ProductId(String value) {

public ProductId {
Objects.requireNonNull(value, "Product ID cannot be null");
if (value.isBlank()) {
throw new InvalidProductIdException(value);
}
if (!value.matches("^[0-9]+$")) {
throw new InvalidProductIdException(value);
}
}

public static ProductId of(String value) {
return new ProductId(value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.zara.similarproducts.domain.model;

public class ProductNotFoundException extends DomainException {

private static final String ERROR_CODE = "PRODUCT_NOT_FOUND";
private final ProductId productId;

public ProductNotFoundException(ProductId productId) {
super("Product not found: " + productId.value());
this.productId = productId;
}

public ProductId getProductId() {
return productId;
}

@Override
public String getErrorCode() {
return ERROR_CODE;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.zara.similarproducts.domain.model;

import java.util.List;
import java.util.Objects;

public record SimilarProducts(List<Product> products) {

public SimilarProducts {
Objects.requireNonNull(products, "Products list cannot be null");
}

public static SimilarProducts of(List<Product> products) {
Objects.requireNonNull(products, "Products list cannot be null");
return new SimilarProducts(List.copyOf(products));
}

public static SimilarProducts empty() {
return new SimilarProducts(List.of());
}

public boolean isEmpty() {
return products.isEmpty();
}

public int size() {
return products.size();
}
}
Loading