diff --git a/adapter/pom.xml b/adapter/pom.xml
index c56fdf2..ba2ab42 100644
--- a/adapter/pom.xml
+++ b/adapter/pom.xml
@@ -34,6 +34,10 @@
org.springframework.boot
spring-boot-starter-data-jpa
+
+ org.springframework.boot
+ spring-boot-starter-data-mongodb
+
com.mysql
mysql-connector-j
@@ -61,6 +65,22 @@
mysql
test
+
+ org.testcontainers
+ mongodb
+ test
+
+
+ org.testcontainers
+ testcontainers
+ test
+
+
+ org.testcontainers
+ junit-jupiter
+ test
+
+
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/mongo/CartLineItemMongoEntity.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/mongo/CartLineItemMongoEntity.java
new file mode 100644
index 0000000..805806a
--- /dev/null
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/mongo/CartLineItemMongoEntity.java
@@ -0,0 +1,21 @@
+package eu.happycoders.shop.adapter.out.persistence.mongo;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.data.mongodb.core.mapping.DBRef;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+/**
+ * MongoDB document class for a shopping cart line item.
+ *
+ * @author Alfredo Rueda & Francisco José Nebrera
+ */
+@Document(collection = "CartLineItem")
+@Getter
+@Setter
+public class CartLineItemMongoEntity {
+
+ @DBRef private ProductMongoEntity product;
+
+ private int quantity;
+}
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/mongo/CartMapper.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/mongo/CartMapper.java
new file mode 100644
index 0000000..f27c330
--- /dev/null
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/mongo/CartMapper.java
@@ -0,0 +1,52 @@
+package eu.happycoders.shop.adapter.out.persistence.mongo;
+
+import eu.happycoders.shop.model.cart.Cart;
+import eu.happycoders.shop.model.cart.CartLineItem;
+import eu.happycoders.shop.model.customer.CustomerId;
+
+/**
+ * Maps model carts and line items to MongoDB carts and line items - and vice versa.
+ *
+ * @author Alfredo Rueda & Francisco José Nebrera
+ */
+final class CartMapper {
+
+ private CartMapper() {}
+
+ static CartMongoEntity toMongoEntity(Cart cart) {
+ CartMongoEntity cartMongoEntity = new CartMongoEntity();
+ cartMongoEntity.setCustomerId(cart.id().value());
+
+ cartMongoEntity.setLineItems(
+ cart.lineItems().stream()
+ .map(lineItem -> toMongoEntity(cartMongoEntity, lineItem))
+ .toList());
+
+ return cartMongoEntity;
+ }
+
+ static CartLineItemMongoEntity toMongoEntity(
+ CartMongoEntity cartMongoEntity, CartLineItem lineItem) {
+ ProductMongoEntity productMongoEntity = new ProductMongoEntity();
+ productMongoEntity.setId(lineItem.product().id().value());
+
+ CartLineItemMongoEntity entity = new CartLineItemMongoEntity();
+ entity.setProduct(productMongoEntity);
+ entity.setQuantity(lineItem.quantity());
+
+ return entity;
+ }
+
+ static Cart toModelEntity(CartMongoEntity cartMongoEntity) {
+ CustomerId customerId = new CustomerId(cartMongoEntity.getCustomerId());
+ Cart cart = new Cart(customerId);
+
+ for (CartLineItemMongoEntity lineItemMongoEntity : cartMongoEntity.getLineItems()) {
+ cart.putProductIgnoringNotEnoughItemsInStock(
+ ProductMapper.toModelEntity(lineItemMongoEntity.getProduct()),
+ lineItemMongoEntity.getQuantity());
+ }
+
+ return cart;
+ }
+}
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/mongo/CartMongoEntity.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/mongo/CartMongoEntity.java
new file mode 100644
index 0000000..e9ccff0
--- /dev/null
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/mongo/CartMongoEntity.java
@@ -0,0 +1,23 @@
+package eu.happycoders.shop.adapter.out.persistence.mongo;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+import java.util.List;
+
+/**
+ * MongoDB document class for a shopping cart.
+ *
+ * @author Alfredo Rueda & Francisco José Nebrera
+ */
+@Document(collection = "Cart")
+@Getter
+@Setter
+public class CartMongoEntity {
+
+ @Id private int customerId;
+
+ private List lineItems;
+}
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/mongo/MongoCartRepository.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/mongo/MongoCartRepository.java
new file mode 100644
index 0000000..b83f8df
--- /dev/null
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/mongo/MongoCartRepository.java
@@ -0,0 +1,46 @@
+package eu.happycoders.shop.adapter.out.persistence.mongo;
+
+import eu.happycoders.shop.application.port.out.persistence.CartRepository;
+import eu.happycoders.shop.model.cart.Cart;
+import eu.happycoders.shop.model.customer.CustomerId;
+import jakarta.transaction.Transactional;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+/**
+ * Persistence adapter: Stores carts via MongoDB in a database.
+ * @author Alfredo Rueda & Francisco José Nebrera
+ */
+@ConditionalOnProperty(name = "persistence", havingValue = "mongodb")
+@Repository
+public class MongoCartRepository implements CartRepository {
+
+ private final MongoCartSpringDataRepository mongoCartSpringDataRepository;
+
+ public MongoCartRepository(
+ MongoCartSpringDataRepository mongoCartSpringDataRepository) {
+ this.mongoCartSpringDataRepository = mongoCartSpringDataRepository;
+ }
+
+ @Override
+ @Transactional
+ public void save(Cart cart) {
+ mongoCartSpringDataRepository.save(CartMapper.toMongoEntity(cart));
+ }
+
+ @Override
+ @Transactional
+ public Optional findByCustomerId(CustomerId customerId) {
+ Optional cartMongoEntity =
+ mongoCartSpringDataRepository.findById(customerId.value());
+ return cartMongoEntity.map(CartMapper::toModelEntity);
+ }
+
+ @Override
+ @Transactional
+ public void deleteByCustomerId(CustomerId customerId) {
+ mongoCartSpringDataRepository.deleteById(customerId.value());
+ }
+}
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/mongo/MongoCartSpringDataRepository.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/mongo/MongoCartSpringDataRepository.java
new file mode 100644
index 0000000..35cfaff
--- /dev/null
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/mongo/MongoCartSpringDataRepository.java
@@ -0,0 +1,12 @@
+package eu.happycoders.shop.adapter.out.persistence.mongo;
+
+import org.springframework.data.mongodb.repository.MongoRepository;
+import org.springframework.stereotype.Repository;
+
+/**
+ * Spring Data repository for {@link CartMongoEntity}.
+ *
+ * @author Alfredo Rueda & Francisco José Nebrera
+ */
+@Repository
+public interface MongoCartSpringDataRepository extends MongoRepository {}
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/mongo/MongoProductRepository.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/mongo/MongoProductRepository.java
new file mode 100644
index 0000000..e33065b
--- /dev/null
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/mongo/MongoProductRepository.java
@@ -0,0 +1,56 @@
+package eu.happycoders.shop.adapter.out.persistence.mongo;
+
+import eu.happycoders.shop.adapter.out.persistence.DemoProducts;
+import eu.happycoders.shop.application.port.out.persistence.ProductRepository;
+import eu.happycoders.shop.model.product.Product;
+import eu.happycoders.shop.model.product.ProductId;
+import jakarta.annotation.PostConstruct;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Persistence adapter: Stores products via MongoDB.
+ *
+ * @author Alfredo Rueda & Francisco José Nebrera
+ */
+@ConditionalOnProperty(name = "persistence", havingValue = "mongodb")
+@Repository
+public class MongoProductRepository implements ProductRepository {
+
+ private final MongoProductSpringDataRepository springDataRepository;
+
+ public MongoProductRepository(MongoProductSpringDataRepository springDataRepository) {
+ this.springDataRepository = springDataRepository;
+ }
+
+ @PostConstruct
+ void createDemoProducts() {
+ DemoProducts.DEMO_PRODUCTS.forEach(this::save);
+ }
+
+ @Override
+ @Transactional
+ public void save(Product product) {
+ springDataRepository.save(ProductMapper.toMongoEntity(product));
+ }
+
+ @Override
+ @Transactional
+ public Optional findById(ProductId productId) {
+ Optional mongoEntity = springDataRepository.findById(productId.value());
+ return mongoEntity.map(ProductMapper::toModelEntity);
+ }
+
+ @Override
+ @Transactional
+ public List findByNameOrDescription(String queryString) {
+ List entities =
+ springDataRepository.findByNameOrDescriptionLike(queryString);
+
+ return ProductMapper.toModelEntities(entities);
+ }
+}
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/mongo/MongoProductSpringDataRepository.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/mongo/MongoProductSpringDataRepository.java
new file mode 100644
index 0000000..4cad5e0
--- /dev/null
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/mongo/MongoProductSpringDataRepository.java
@@ -0,0 +1,21 @@
+package eu.happycoders.shop.adapter.out.persistence.mongo;
+
+import org.springframework.data.mongodb.repository.MongoRepository;
+import org.springframework.data.mongodb.repository.Query;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+/**
+ * Spring Data repository for {@link ProductMongoEntity}.
+ *
+ * @author Alfredo Rueda & Francisco José Nebrera
+ */
+@Repository
+public interface MongoProductSpringDataRepository
+ extends MongoRepository {
+
+ @Query("{ '$or': [ { 'name': { '$regex': ?0, '$options': 'i' } }, { 'description': { '$regex': ?0, '$options': 'i' } } ] }")
+ List findByNameOrDescriptionLike(String pattern);
+
+}
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/mongo/ProductMapper.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/mongo/ProductMapper.java
new file mode 100644
index 0000000..f03a46d
--- /dev/null
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/mongo/ProductMapper.java
@@ -0,0 +1,45 @@
+package eu.happycoders.shop.adapter.out.persistence.mongo;
+
+import eu.happycoders.shop.model.money.Money;
+import eu.happycoders.shop.model.product.Product;
+import eu.happycoders.shop.model.product.ProductId;
+
+import java.util.Currency;
+import java.util.List;
+
+/**
+ * Maps a model product to a MongoDB product and vice versa.
+ *
+ * @author Alfredo Rueda & Francisco José Nebrera
+ */
+final class ProductMapper {
+
+ private ProductMapper() {}
+
+ static ProductMongoEntity toMongoEntity(Product product) {
+ ProductMongoEntity mongoEntity = new ProductMongoEntity();
+
+ mongoEntity.setId(product.id().value());
+ mongoEntity.setName(product.name());
+ mongoEntity.setDescription(product.description());
+ mongoEntity.setPriceCurrency(product.price().currency().getCurrencyCode());
+ mongoEntity.setPriceAmount(product.price().amount());
+ mongoEntity.setItemsInStock(product.itemsInStock());
+
+ return mongoEntity;
+ }
+
+ static Product toModelEntity(ProductMongoEntity mongoEntity) {
+ return new Product(
+ new ProductId(mongoEntity.getId()),
+ mongoEntity.getName(),
+ mongoEntity.getDescription(),
+ new Money(
+ Currency.getInstance(mongoEntity.getPriceCurrency()), mongoEntity.getPriceAmount()),
+ mongoEntity.getItemsInStock());
+ }
+
+ static List toModelEntities(List mongoEntities) {
+ return mongoEntities.stream().map(ProductMapper::toModelEntity).toList();
+ }
+}
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/mongo/ProductMongoEntity.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/mongo/ProductMongoEntity.java
new file mode 100644
index 0000000..6dd8665
--- /dev/null
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/mongo/ProductMongoEntity.java
@@ -0,0 +1,37 @@
+package eu.happycoders.shop.adapter.out.persistence.mongo;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.mapping.Document;
+import org.springframework.data.mongodb.core.mapping.Field;
+
+import java.math.BigDecimal;
+
+/**
+ * MongoDB document class for a product.
+ *
+ * @author Alfredo Rueda & Francisco José Nebrera
+ */
+@Document(collection = "Product")
+@Getter
+@Setter
+public class ProductMongoEntity {
+
+ @Id private String id;
+
+ @Field("name")
+ private String name;
+
+ @Field("description")
+ private String description;
+
+ @Field("price_currency")
+ private String priceCurrency;
+
+ @Field("price_amount")
+ private BigDecimal priceAmount;
+
+ @Field("items_in_stock")
+ private int itemsInStock;
+}
diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/mongo/MongoDBCartRepositoryTest.java b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/mongo/MongoDBCartRepositoryTest.java
new file mode 100644
index 0000000..7fd949d
--- /dev/null
+++ b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/mongo/MongoDBCartRepositoryTest.java
@@ -0,0 +1,19 @@
+package eu.happycoders.shop.adapter.out.persistence.mongo;
+
+import eu.happycoders.shop.adapter.out.persistence.AbstractCartRepositoryTest;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.DynamicPropertyRegistry;
+import org.springframework.test.context.DynamicPropertySource;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+@SpringBootTest
+@ActiveProfiles("test-with-mongodb")
+class MongoDBCartRepositoryTest extends AbstractCartRepositoryTest {
+
+ @DynamicPropertySource
+ static void mongoDbProperties(DynamicPropertyRegistry registry) {
+ registry.add("spring.data.mongodb.uri", () -> MongoDBTestContainerConfig.getInstance().getReplicaSetUrl());
+ }
+}
diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/mongo/MongoDBTestContainerConfig.java b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/mongo/MongoDBTestContainerConfig.java
new file mode 100644
index 0000000..20319c4
--- /dev/null
+++ b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/mongo/MongoDBTestContainerConfig.java
@@ -0,0 +1,23 @@
+package eu.happycoders.shop.adapter.out.persistence.mongo;
+
+import org.springframework.context.annotation.Profile;
+import org.testcontainers.containers.MongoDBContainer;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+@Profile("test-with-mongodb")
+@Testcontainers
+public class MongoDBTestContainerConfig {
+
+ // See: https://java.testcontainers.org/test_framework_integration/manual_lifecycle_control/#singleton-containers
+
+ private static final MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4.2.8");
+
+ static {
+ mongoDBContainer.start();
+ }
+
+ public static MongoDBContainer getInstance() {
+ return mongoDBContainer;
+ }
+
+}
\ No newline at end of file
diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/mongo/MongoProductRepositoryTest.java b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/mongo/MongoProductRepositoryTest.java
new file mode 100644
index 0000000..2a5af17
--- /dev/null
+++ b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/mongo/MongoProductRepositoryTest.java
@@ -0,0 +1,19 @@
+package eu.happycoders.shop.adapter.out.persistence.mongo;
+
+import eu.happycoders.shop.adapter.out.persistence.AbstractProductRepositoryTest;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.DynamicPropertyRegistry;
+import org.springframework.test.context.DynamicPropertySource;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+@SpringBootTest
+@ActiveProfiles("test-with-mongodb")
+class MongoProductRepositoryTest extends AbstractProductRepositoryTest {
+
+ @DynamicPropertySource
+ static void mongoDbProperties(DynamicPropertyRegistry registry) {
+ registry.add("spring.data.mongodb.uri", () -> MongoDBTestContainerConfig.getInstance().getReplicaSetUrl());
+ }
+}
diff --git a/adapter/src/test/resources/application-test-with-mongodb.properties b/adapter/src/test/resources/application-test-with-mongodb.properties
new file mode 100644
index 0000000..49b0df0
--- /dev/null
+++ b/adapter/src/test/resources/application-test-with-mongodb.properties
@@ -0,0 +1,5 @@
+# spring.data.mongodb.uri is set to a test container via Java code
+# because is not possible to set up a test container in a properties file with MongoDB (as it is with MySQL)
+persistence=mongodb
+
+
diff --git a/bootstrap/pom.xml b/bootstrap/pom.xml
index 5e3a376..e702ce3 100644
--- a/bootstrap/pom.xml
+++ b/bootstrap/pom.xml
@@ -53,6 +53,16 @@
mysql
test
+
+ org.testcontainers
+ mongodb
+ test
+
+
+ org.testcontainers
+ testcontainers
+ test
+
diff --git a/bootstrap/src/main/resources/application-mongodb.properties b/bootstrap/src/main/resources/application-mongodb.properties
new file mode 100644
index 0000000..46efdba
--- /dev/null
+++ b/bootstrap/src/main/resources/application-mongodb.properties
@@ -0,0 +1,5 @@
+spring.data.mongodb.database=shop
+spring.data.mongodb.uri=mongodb://localhost:27017/shop
+
+persistence=mongodb
+
diff --git a/bootstrap/src/test/java/eu/happycoders/shop/bootstrap/e2e/CartTest.java b/bootstrap/src/test/java/eu/happycoders/shop/bootstrap/e2e/CartTest.java
index 111b362..56ee4f1 100644
--- a/bootstrap/src/test/java/eu/happycoders/shop/bootstrap/e2e/CartTest.java
+++ b/bootstrap/src/test/java/eu/happycoders/shop/bootstrap/e2e/CartTest.java
@@ -21,6 +21,8 @@
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test-with-mysql")
+// Uncomment the following line to run the tests with MongoDB (and comment the previous line)
+//@ActiveProfiles("test-with-mongodb")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class CartTest {
diff --git a/bootstrap/src/test/java/eu/happycoders/shop/bootstrap/e2e/FindProductsTest.java b/bootstrap/src/test/java/eu/happycoders/shop/bootstrap/e2e/FindProductsTest.java
index 3a152b7..6ae74d5 100644
--- a/bootstrap/src/test/java/eu/happycoders/shop/bootstrap/e2e/FindProductsTest.java
+++ b/bootstrap/src/test/java/eu/happycoders/shop/bootstrap/e2e/FindProductsTest.java
@@ -15,6 +15,8 @@
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test-with-mysql")
+// Uncomment the following line to run the tests with MongoDB (and comment the previous line)
+//@ActiveProfiles("test-with-mongodb")
class FindProductsTest {
@LocalServerPort private Integer port;