+ }
+}
+DesktopBff ..|> ClientBff
+MobileBff ..|> ClientBff
+DesktopBff --> "-authService" AuthService
+DesktopBff --> "-orderService" OrderService
+DesktopBff --> "-supplierService" SupplierService
+MobileBff --> "-authService" AuthService
+MobileBff --> "-cartService" CartService
+MobileBff --> "-orderService" OrderService
+InMemoryAuthService ..|> AuthService
+InMemoryCartService ..|> CartService
+InMemoryOrderService ..|> OrderService
+InMemorySupplierService ..|> SupplierService
+CartItem --> "-product" Product
+@enduml
diff --git a/backends-for-frontends/pom.xml b/backends-for-frontends/pom.xml
new file mode 100644
index 000000000000..8f1e454fa646
--- /dev/null
+++ b/backends-for-frontends/pom.xml
@@ -0,0 +1,67 @@
+
+
+
+ 4.0.0
+
+ com.iluwatar
+ java-design-patterns
+ 1.26.0-SNAPSHOT
+
+
+ backends-for-frontends
+
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+ com.iluwatar.bff.App
+
+
+
+
+
+
+
diff --git a/backends-for-frontends/src/main/java/com/iluwatar/bff/App.java b/backends-for-frontends/src/main/java/com/iluwatar/bff/App.java
new file mode 100644
index 000000000000..b76939c53fd4
--- /dev/null
+++ b/backends-for-frontends/src/main/java/com/iluwatar/bff/App.java
@@ -0,0 +1,109 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bff;
+
+import com.iluwatar.bff.bff.DesktopBff;
+import com.iluwatar.bff.bff.MobileBff;
+import com.iluwatar.bff.model.CartItem;
+import com.iluwatar.bff.model.Order;
+import com.iluwatar.bff.model.Product;
+import com.iluwatar.bff.model.SupplierRecord;
+import com.iluwatar.bff.model.User;
+import com.iluwatar.bff.service.impl.InMemoryAuthService;
+import com.iluwatar.bff.service.impl.InMemoryCartService;
+import com.iluwatar.bff.service.impl.InMemoryOrderService;
+import com.iluwatar.bff.service.impl.InMemorySupplierService;
+import java.util.List;
+import java.util.Map;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * {@link App} demonstrates the Backends For Frontends (BFF) pattern.
+ *
+ * A single set of downstream microservices (customer authentication, cart, order and supplier
+ * services) is shared by every client. Two different client-facing gateways -- {@link MobileBff}
+ * for the mobile apps and {@link DesktopBff} for the intranet desktop/chatbot clients -- each call
+ * a different subset of those services and reshape the results into a response tailored to what
+ * their own client actually needs, rather than exposing one one-size-fits-all API to every client.
+ */
+public final class App {
+
+ /** Logger for this class. */
+ private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
+
+ /** User id used in the demonstration. */
+ private static final String USER_ID = "u-1";
+
+ /** Product id used in the demonstration. */
+ private static final String PRODUCT_ID = "p-42";
+
+ /** Unit price of the demo product in US dollars. */
+ private static final double DEMO_PRICE_USD = 79.99;
+
+ /** Supplier stock level used in the demonstration. */
+ private static final int DEMO_STOCK_LEVEL = 120;
+
+ private App() {
+ // utility class
+ }
+
+ /**
+ * Program entry point.
+ *
+ * @param args no argument sent
+ */
+ public static void main(final String[] args) {
+ // shared downstream microservices, as drawn in the pattern diagram
+ var authService = new InMemoryAuthService(Map.of(USER_ID, new User(USER_ID, "Alice", "GOLD")));
+
+ var product = new Product(PRODUCT_ID, "Wireless Headphones", DEMO_PRICE_USD);
+ var cartService = new InMemoryCartService(Map.of(USER_ID, List.of(new CartItem(product, 2))));
+
+ var orderService =
+ new InMemoryOrderService(
+ Map.of(
+ USER_ID,
+ List.of(
+ new Order("o-1", "Wireless Headphones", "DELIVERED"),
+ new Order("o-2", "USB-C Cable", "IN_TRANSIT"))));
+
+ var supplierService =
+ new InMemorySupplierService(
+ Map.of(
+ "Wireless Headphones",
+ List.of(new SupplierRecord(PRODUCT_ID, "Acme Audio Co.", DEMO_STOCK_LEVEL))));
+
+ // client-specific BFFs, each calling only the services their client needs
+ var mobileBff = new MobileBff(authService, cartService, orderService);
+ var desktopBff = new DesktopBff(authService, orderService, supplierService);
+
+ var mobileResponse = mobileBff.getDashboard(USER_ID);
+ LOGGER.info("Mobile BFF response: {}", mobileResponse);
+
+ var desktopResponse = desktopBff.getDashboard(USER_ID);
+ LOGGER.info("Desktop BFF response: {}", desktopResponse);
+ }
+}
diff --git a/backends-for-frontends/src/main/java/com/iluwatar/bff/bff/ClientBff.java b/backends-for-frontends/src/main/java/com/iluwatar/bff/bff/ClientBff.java
new file mode 100644
index 000000000000..4c1d8d768ecb
--- /dev/null
+++ b/backends-for-frontends/src/main/java/com/iluwatar/bff/bff/ClientBff.java
@@ -0,0 +1,44 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bff.bff;
+
+/**
+ * Common contract every client-specific Backend For Frontend implements: given a user id, build
+ * whatever response shape ({@code T}) that particular client needs. Each implementation is free to
+ * call a different subset of downstream services and aggregate them differently -- that freedom is
+ * the entire point of the pattern.
+ *
+ * @param the response DTO shape this BFF returns to its client
+ */
+public interface ClientBff {
+
+ /**
+ * Builds the dashboard response for a given user, tailored to this BFF's client.
+ *
+ * @param userId identifier of the user requesting their dashboard
+ * @return the client-specific response
+ */
+ T getDashboard(String userId);
+}
diff --git a/backends-for-frontends/src/main/java/com/iluwatar/bff/bff/DesktopBff.java b/backends-for-frontends/src/main/java/com/iluwatar/bff/bff/DesktopBff.java
new file mode 100644
index 000000000000..1073f4556558
--- /dev/null
+++ b/backends-for-frontends/src/main/java/com/iluwatar/bff/bff/DesktopBff.java
@@ -0,0 +1,94 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bff.bff;
+
+import com.iluwatar.bff.dto.DesktopDashboardResponse;
+import com.iluwatar.bff.service.AuthService;
+import com.iluwatar.bff.service.OrderService;
+import com.iluwatar.bff.service.SupplierService;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Backend For Frontend serving the intranet clients (desktop app, chatbot) from the diagram. It
+ * aggregates the auth, order and supplier services and reshapes the results into a richer,
+ * back-office style payload. Unlike {@link MobileBff}, it does not touch the cart service at all
+ * and it does reach into the intranet-only supplier service, matching the fan-out drawn in the
+ * pattern diagram.
+ */
+public final class DesktopBff implements ClientBff {
+
+ /** The customer authentication service. */
+ private final AuthService authService;
+
+ /** The order service. */
+ private final OrderService orderService;
+
+ /** The supplier service (intranet-only). */
+ private final SupplierService supplierService;
+
+ /**
+ * Creates a desktop BFF wired to the downstream services it depends on.
+ *
+ * @param auth the customer authentication service
+ * @param orders the order service
+ * @param suppliers the supplier service
+ */
+ public DesktopBff(
+ final AuthService auth, final OrderService orders, final SupplierService suppliers) {
+ this.authService = auth;
+ this.orderService = orders;
+ this.supplierService = suppliers;
+ }
+
+ @Override
+ public DesktopDashboardResponse getDashboard(final String userId) {
+ var user = authService.getUser(userId);
+ var orders = orderService.getOrders(userId);
+
+ var orderStatuses =
+ orders.stream()
+ .map(order -> order.id() + ": " + order.productName() + " [" + order.status() + "]")
+ .toList();
+
+ var supplierStockSummaries = new ArrayList();
+ for (var order : orders) {
+ for (var supplierRecord : supplierService.getSupplierRecords(order.productName())) {
+ supplierStockSummaries.add(
+ supplierRecord.supplierName()
+ + ": "
+ + supplierRecord.stockLevel()
+ + " units of "
+ + order.productName());
+ }
+ }
+
+ return new DesktopDashboardResponse(
+ "Welcome back, " + user.displayName(),
+ user.loyaltyTier(),
+ List.copyOf(orderStatuses),
+ List.copyOf(supplierStockSummaries));
+ }
+}
diff --git a/backends-for-frontends/src/main/java/com/iluwatar/bff/bff/MobileBff.java b/backends-for-frontends/src/main/java/com/iluwatar/bff/bff/MobileBff.java
new file mode 100644
index 000000000000..a51a7c8a8201
--- /dev/null
+++ b/backends-for-frontends/src/main/java/com/iluwatar/bff/bff/MobileBff.java
@@ -0,0 +1,86 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bff.bff;
+
+import com.iluwatar.bff.dto.MobileDashboardResponse;
+import com.iluwatar.bff.service.AuthService;
+import com.iluwatar.bff.service.CartService;
+import com.iluwatar.bff.service.OrderService;
+import java.util.List;
+
+/**
+ * Backend For Frontend serving the mobile clients (iOS app, Android app) from the diagram. It
+ * aggregates the auth, cart and order services and reshapes the results into a small payload suited
+ * to a phone screen and a limited-bandwidth connection. It never calls the supplier service: a
+ * mobile shopper has no use for back-office stock data, so this BFF simply does not expose it,
+ * rather than sending it and letting the client ignore it.
+ */
+public final class MobileBff implements ClientBff {
+
+ /** Maximum number of recent order summaries to include in the mobile response. */
+ private static final int MAX_RECENT_ORDERS = 3;
+
+ /** The customer authentication service. */
+ private final AuthService authService;
+
+ /** The cart service. */
+ private final CartService cartService;
+
+ /** The order service. */
+ private final OrderService orderService;
+
+ /**
+ * Creates a mobile BFF wired to the downstream services it depends on.
+ *
+ * @param auth the customer authentication service
+ * @param cart the cart service
+ * @param orders the order service
+ */
+ public MobileBff(final AuthService auth, final CartService cart, final OrderService orders) {
+ this.authService = auth;
+ this.cartService = cart;
+ this.orderService = orders;
+ }
+
+ @Override
+ public MobileDashboardResponse getDashboard(final String userId) {
+ var user = authService.getUser(userId);
+ var cart = cartService.getCart(userId);
+ var orders = orderService.getOrders(userId);
+
+ var cartTotal = cart.stream().mapToDouble(item -> item.lineTotal()).sum();
+ var recentOrderSummaries =
+ orders.stream()
+ .limit(MAX_RECENT_ORDERS)
+ .map(order -> order.productName() + " (" + order.status() + ")")
+ .toList();
+
+ return new MobileDashboardResponse(
+ "Hi " + user.displayName() + "!",
+ cart.size(),
+ cartTotal,
+ List.copyOf(recentOrderSummaries));
+ }
+}
diff --git a/backends-for-frontends/src/main/java/com/iluwatar/bff/bff/package-info.java b/backends-for-frontends/src/main/java/com/iluwatar/bff/bff/package-info.java
new file mode 100644
index 000000000000..c338c3158023
--- /dev/null
+++ b/backends-for-frontends/src/main/java/com/iluwatar/bff/bff/package-info.java
@@ -0,0 +1,29 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+/**
+ * Client-specific Backend For Frontend implementations: one per client type (mobile, desktop) that
+ * each aggregate a different subset of downstream services.
+ */
+package com.iluwatar.bff.bff;
diff --git a/backends-for-frontends/src/main/java/com/iluwatar/bff/dto/DesktopDashboardResponse.java b/backends-for-frontends/src/main/java/com/iluwatar/bff/dto/DesktopDashboardResponse.java
new file mode 100644
index 000000000000..434e1f1cc088
--- /dev/null
+++ b/backends-for-frontends/src/main/java/com/iluwatar/bff/dto/DesktopDashboardResponse.java
@@ -0,0 +1,42 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bff.dto;
+
+import java.util.List;
+
+/**
+ * The shape of data the desktop back-office client renders: richer than the mobile payload,
+ * including full order status and supplier stock levels that a mobile shopper never needs.
+ *
+ * @param greeting a short personalized greeting for the user
+ * @param loyaltyTier the user's loyalty program tier
+ * @param orderStatuses detailed "id: productName [status]" lines for every order
+ * @param supplierStockSummaries "supplierName: stockLevel units" lines for relevant products
+ */
+public record DesktopDashboardResponse(
+ String greeting,
+ String loyaltyTier,
+ List orderStatuses,
+ List supplierStockSummaries) {}
diff --git a/backends-for-frontends/src/main/java/com/iluwatar/bff/dto/MobileDashboardResponse.java b/backends-for-frontends/src/main/java/com/iluwatar/bff/dto/MobileDashboardResponse.java
new file mode 100644
index 000000000000..d6d27619c36d
--- /dev/null
+++ b/backends-for-frontends/src/main/java/com/iluwatar/bff/dto/MobileDashboardResponse.java
@@ -0,0 +1,39 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bff.dto;
+
+import java.util.List;
+
+/**
+ * The shape of data the mobile (iOS/Android) client actually renders: a lean payload with just
+ * enough for a small screen, deliberately excluding fields the desktop client needs.
+ *
+ * @param greeting a short personalized greeting for the user
+ * @param cartItemCount number of items currently in the user's cart
+ * @param cartTotalUsd total value of the user's cart in US dollars
+ * @param recentOrderSummaries short human-readable summaries of the user's most recent orders
+ */
+public record MobileDashboardResponse(
+ String greeting, int cartItemCount, double cartTotalUsd, List recentOrderSummaries) {}
diff --git a/backends-for-frontends/src/main/java/com/iluwatar/bff/dto/package-info.java b/backends-for-frontends/src/main/java/com/iluwatar/bff/dto/package-info.java
new file mode 100644
index 000000000000..43a23d0dbc4e
--- /dev/null
+++ b/backends-for-frontends/src/main/java/com/iluwatar/bff/dto/package-info.java
@@ -0,0 +1,28 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+/**
+ * Response DTOs shaped by each BFF for its specific client (mobile dashboard, desktop dashboard).
+ */
+package com.iluwatar.bff.dto;
diff --git a/backends-for-frontends/src/main/java/com/iluwatar/bff/model/CartItem.java b/backends-for-frontends/src/main/java/com/iluwatar/bff/model/CartItem.java
new file mode 100644
index 000000000000..a71c4848c791
--- /dev/null
+++ b/backends-for-frontends/src/main/java/com/iluwatar/bff/model/CartItem.java
@@ -0,0 +1,43 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bff.model;
+
+/**
+ * A single line item inside a user's shopping cart, as returned by the cart service API.
+ *
+ * @param product the product being purchased
+ * @param quantity number of units of the product
+ */
+public record CartItem(Product product, int quantity) {
+
+ /**
+ * Computes the line total for this cart item.
+ *
+ * @return quantity multiplied by unit price
+ */
+ public double lineTotal() {
+ return quantity * product.priceUsd();
+ }
+}
diff --git a/backends-for-frontends/src/main/java/com/iluwatar/bff/model/Order.java b/backends-for-frontends/src/main/java/com/iluwatar/bff/model/Order.java
new file mode 100644
index 000000000000..02a2026695b0
--- /dev/null
+++ b/backends-for-frontends/src/main/java/com/iluwatar/bff/model/Order.java
@@ -0,0 +1,34 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bff.model;
+
+/**
+ * A past order returned by the order service API.
+ *
+ * @param id order identifier
+ * @param productName name of the ordered product
+ * @param status current fulfillment status, e.g. "DELIVERED", "IN_TRANSIT"
+ */
+public record Order(String id, String productName, String status) {}
diff --git a/backends-for-frontends/src/main/java/com/iluwatar/bff/model/Product.java b/backends-for-frontends/src/main/java/com/iluwatar/bff/model/Product.java
new file mode 100644
index 000000000000..ae4176860b6e
--- /dev/null
+++ b/backends-for-frontends/src/main/java/com/iluwatar/bff/model/Product.java
@@ -0,0 +1,35 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bff.model;
+
+/**
+ * Simple product record returned by the downstream Cart/Order services. Represents the data a
+ * catalog or cart entry carries before each BFF trims or reshapes it for its own client.
+ *
+ * @param id product identifier
+ * @param name display name of the product
+ * @param priceUsd unit price in US dollars
+ */
+public record Product(String id, String name, double priceUsd) {}
diff --git a/backends-for-frontends/src/main/java/com/iluwatar/bff/model/SupplierRecord.java b/backends-for-frontends/src/main/java/com/iluwatar/bff/model/SupplierRecord.java
new file mode 100644
index 000000000000..f7dfa38fe89d
--- /dev/null
+++ b/backends-for-frontends/src/main/java/com/iluwatar/bff/model/SupplierRecord.java
@@ -0,0 +1,35 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bff.model;
+
+/**
+ * A supplier stock record returned by the supplier service API, used by the desktop back-office
+ * client to show inventory information that mobile customers never need to see.
+ *
+ * @param productId identifier of the product this record refers to
+ * @param supplierName name of the supplying vendor
+ * @param stockLevel units currently available from this supplier
+ */
+public record SupplierRecord(String productId, String supplierName, int stockLevel) {}
diff --git a/backends-for-frontends/src/main/java/com/iluwatar/bff/model/User.java b/backends-for-frontends/src/main/java/com/iluwatar/bff/model/User.java
new file mode 100644
index 000000000000..5c59c7f963ec
--- /dev/null
+++ b/backends-for-frontends/src/main/java/com/iluwatar/bff/model/User.java
@@ -0,0 +1,35 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bff.model;
+
+/**
+ * Authenticated user profile returned by the customer authentication service API. Kept
+ * intentionally minimal; each BFF decides which of these fields its client actually needs.
+ *
+ * @param id unique user identifier
+ * @param displayName human-readable name of the user
+ * @param loyaltyTier loyalty program tier, e.g. "GOLD", "SILVER", "STANDARD"
+ */
+public record User(String id, String displayName, String loyaltyTier) {}
diff --git a/backends-for-frontends/src/main/java/com/iluwatar/bff/model/package-info.java b/backends-for-frontends/src/main/java/com/iluwatar/bff/model/package-info.java
new file mode 100644
index 000000000000..37d2855a8671
--- /dev/null
+++ b/backends-for-frontends/src/main/java/com/iluwatar/bff/model/package-info.java
@@ -0,0 +1,30 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+/**
+ * Domain model records shared across downstream services: User, Product, CartItem, Order, and
+ * SupplierRecord. Each BFF consumes these raw domain objects and reshapes them into its own
+ * client-specific DTO.
+ */
+package com.iluwatar.bff.model;
diff --git a/backends-for-frontends/src/main/java/com/iluwatar/bff/package-info.java b/backends-for-frontends/src/main/java/com/iluwatar/bff/package-info.java
new file mode 100644
index 000000000000..ede18f2b22f9
--- /dev/null
+++ b/backends-for-frontends/src/main/java/com/iluwatar/bff/package-info.java
@@ -0,0 +1,29 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+/**
+ * Backends For Frontends pattern: a dedicated gateway per client type (mobile, desktop) that
+ * aggregates only the downstream services each client actually needs.
+ */
+package com.iluwatar.bff;
diff --git a/backends-for-frontends/src/main/java/com/iluwatar/bff/service/AuthService.java b/backends-for-frontends/src/main/java/com/iluwatar/bff/service/AuthService.java
new file mode 100644
index 000000000000..a87b75b48bb6
--- /dev/null
+++ b/backends-for-frontends/src/main/java/com/iluwatar/bff/service/AuthService.java
@@ -0,0 +1,43 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bff.service;
+
+import com.iluwatar.bff.model.User;
+
+/**
+ * Represents the customer authentication service API from the diagram: a downstream microservice
+ * shared by every client-specific BFF. Each BFF calls this the same way; only what they do with the
+ * result differs.
+ */
+public interface AuthService {
+
+ /**
+ * Looks up the authenticated user profile for the given id.
+ *
+ * @param userId identifier of the user to look up
+ * @return the matching {@link User}
+ */
+ User getUser(String userId);
+}
diff --git a/backends-for-frontends/src/main/java/com/iluwatar/bff/service/CartService.java b/backends-for-frontends/src/main/java/com/iluwatar/bff/service/CartService.java
new file mode 100644
index 000000000000..7ae24fba64bc
--- /dev/null
+++ b/backends-for-frontends/src/main/java/com/iluwatar/bff/service/CartService.java
@@ -0,0 +1,43 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bff.service;
+
+import com.iluwatar.bff.model.CartItem;
+import java.util.List;
+
+/**
+ * Represents the cart service API from the diagram. In this example only the mobile and android
+ * clients need cart data, so only {@link com.iluwatar.bff.bff.MobileBff} calls this service.
+ */
+public interface CartService {
+
+ /**
+ * Retrieves the current cart contents for a user.
+ *
+ * @param userId identifier of the user whose cart is requested
+ * @return the list of items currently in the user's cart
+ */
+ List getCart(String userId);
+}
diff --git a/backends-for-frontends/src/main/java/com/iluwatar/bff/service/OrderService.java b/backends-for-frontends/src/main/java/com/iluwatar/bff/service/OrderService.java
new file mode 100644
index 000000000000..b8e6ee8fa03e
--- /dev/null
+++ b/backends-for-frontends/src/main/java/com/iluwatar/bff/service/OrderService.java
@@ -0,0 +1,43 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bff.service;
+
+import com.iluwatar.bff.model.Order;
+import java.util.List;
+
+/**
+ * Represents the order service API from the diagram. This downstream service is shared by every
+ * client-specific BFF, matching the fan-out shown for "order service API" in the pattern diagram.
+ */
+public interface OrderService {
+
+ /**
+ * Retrieves the order history for a user.
+ *
+ * @param userId identifier of the user whose orders are requested
+ * @return the list of past orders for the user
+ */
+ List getOrders(String userId);
+}
diff --git a/backends-for-frontends/src/main/java/com/iluwatar/bff/service/SupplierService.java b/backends-for-frontends/src/main/java/com/iluwatar/bff/service/SupplierService.java
new file mode 100644
index 000000000000..ac7c2dfbc849
--- /dev/null
+++ b/backends-for-frontends/src/main/java/com/iluwatar/bff/service/SupplierService.java
@@ -0,0 +1,43 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bff.service;
+
+import com.iluwatar.bff.model.SupplierRecord;
+import java.util.List;
+
+/**
+ * Represents the supplier service API from the diagram, reachable only from the intranet services
+ * server. Only back-office style clients (desktop app, chatbot) call this service.
+ */
+public interface SupplierService {
+
+ /**
+ * Retrieves supplier stock records for a product.
+ *
+ * @param productName name of the product to check stock for
+ * @return the list of supplier stock records for the product
+ */
+ List getSupplierRecords(String productName);
+}
diff --git a/backends-for-frontends/src/main/java/com/iluwatar/bff/service/impl/InMemoryAuthService.java b/backends-for-frontends/src/main/java/com/iluwatar/bff/service/impl/InMemoryAuthService.java
new file mode 100644
index 000000000000..5c2ffc35958f
--- /dev/null
+++ b/backends-for-frontends/src/main/java/com/iluwatar/bff/service/impl/InMemoryAuthService.java
@@ -0,0 +1,58 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bff.service.impl;
+
+import com.iluwatar.bff.model.User;
+import com.iluwatar.bff.service.AuthService;
+import java.util.Map;
+
+/**
+ * In-memory stand-in for the real customer authentication service API. Real deployments would
+ * replace this with an HTTP/gRPC client; the BFFs are written against the {@link AuthService}
+ * interface, so swapping the implementation later requires no change to any BFF.
+ */
+public final class InMemoryAuthService implements AuthService {
+
+ /** Users stored in memory, keyed by user id. */
+ private final Map users;
+
+ /**
+ * Creates the service with a fixed backing map of users, keyed by user id.
+ *
+ * @param userData the user data this service serves
+ */
+ public InMemoryAuthService(final Map userData) {
+ this.users = userData;
+ }
+
+ @Override
+ public User getUser(final String userId) {
+ var user = users.get(userId);
+ if (user == null) {
+ throw new IllegalArgumentException("Unknown user id: " + userId);
+ }
+ return user;
+ }
+}
diff --git a/backends-for-frontends/src/main/java/com/iluwatar/bff/service/impl/InMemoryCartService.java b/backends-for-frontends/src/main/java/com/iluwatar/bff/service/impl/InMemoryCartService.java
new file mode 100644
index 000000000000..fed3b59da2c8
--- /dev/null
+++ b/backends-for-frontends/src/main/java/com/iluwatar/bff/service/impl/InMemoryCartService.java
@@ -0,0 +1,51 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bff.service.impl;
+
+import com.iluwatar.bff.model.CartItem;
+import com.iluwatar.bff.service.CartService;
+import java.util.List;
+import java.util.Map;
+
+/** In-memory stand-in for the real cart service API. */
+public final class InMemoryCartService implements CartService {
+
+ /** Carts stored in memory, keyed by user id. */
+ private final Map> cartsByUserId;
+
+ /**
+ * Creates the service with a fixed backing map of carts, keyed by user id.
+ *
+ * @param carts the cart data this service serves
+ */
+ public InMemoryCartService(final Map> carts) {
+ this.cartsByUserId = carts;
+ }
+
+ @Override
+ public List getCart(final String userId) {
+ return cartsByUserId.getOrDefault(userId, List.of());
+ }
+}
diff --git a/backends-for-frontends/src/main/java/com/iluwatar/bff/service/impl/InMemoryOrderService.java b/backends-for-frontends/src/main/java/com/iluwatar/bff/service/impl/InMemoryOrderService.java
new file mode 100644
index 000000000000..979956d06244
--- /dev/null
+++ b/backends-for-frontends/src/main/java/com/iluwatar/bff/service/impl/InMemoryOrderService.java
@@ -0,0 +1,51 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bff.service.impl;
+
+import com.iluwatar.bff.model.Order;
+import com.iluwatar.bff.service.OrderService;
+import java.util.List;
+import java.util.Map;
+
+/** In-memory stand-in for the real order service API. */
+public final class InMemoryOrderService implements OrderService {
+
+ /** Order histories stored in memory, keyed by user id. */
+ private final Map> ordersByUserId;
+
+ /**
+ * Creates the service with a fixed backing map of order histories, keyed by user id.
+ *
+ * @param orders the order data this service serves
+ */
+ public InMemoryOrderService(final Map> orders) {
+ this.ordersByUserId = orders;
+ }
+
+ @Override
+ public List getOrders(final String userId) {
+ return ordersByUserId.getOrDefault(userId, List.of());
+ }
+}
diff --git a/backends-for-frontends/src/main/java/com/iluwatar/bff/service/impl/InMemorySupplierService.java b/backends-for-frontends/src/main/java/com/iluwatar/bff/service/impl/InMemorySupplierService.java
new file mode 100644
index 000000000000..ac2d351db3a5
--- /dev/null
+++ b/backends-for-frontends/src/main/java/com/iluwatar/bff/service/impl/InMemorySupplierService.java
@@ -0,0 +1,51 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bff.service.impl;
+
+import com.iluwatar.bff.model.SupplierRecord;
+import com.iluwatar.bff.service.SupplierService;
+import java.util.List;
+import java.util.Map;
+
+/** In-memory stand-in for the real supplier service API, reachable only from the intranet. */
+public final class InMemorySupplierService implements SupplierService {
+
+ /** Supplier records stored in memory, keyed by product name. */
+ private final Map> recordsByProductName;
+
+ /**
+ * Creates the service with a fixed backing map of supplier records, keyed by product name.
+ *
+ * @param records the supplier data this service serves
+ */
+ public InMemorySupplierService(final Map> records) {
+ this.recordsByProductName = records;
+ }
+
+ @Override
+ public List getSupplierRecords(final String productName) {
+ return recordsByProductName.getOrDefault(productName, List.of());
+ }
+}
diff --git a/backends-for-frontends/src/main/java/com/iluwatar/bff/service/impl/package-info.java b/backends-for-frontends/src/main/java/com/iluwatar/bff/service/impl/package-info.java
new file mode 100644
index 000000000000..fbdb5d026bc8
--- /dev/null
+++ b/backends-for-frontends/src/main/java/com/iluwatar/bff/service/impl/package-info.java
@@ -0,0 +1,29 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+/**
+ * In-memory implementations of the downstream service API interfaces, used for the pattern
+ * demonstration. Production deployments would replace these with real HTTP/gRPC clients.
+ */
+package com.iluwatar.bff.service.impl;
diff --git a/backends-for-frontends/src/main/java/com/iluwatar/bff/service/package-info.java b/backends-for-frontends/src/main/java/com/iluwatar/bff/service/package-info.java
new file mode 100644
index 000000000000..05bfcb3911df
--- /dev/null
+++ b/backends-for-frontends/src/main/java/com/iluwatar/bff/service/package-info.java
@@ -0,0 +1,29 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+/**
+ * Downstream service API interfaces shared by all BFFs: AuthService, CartService, OrderService, and
+ * SupplierService. Each BFF wires in only the services its client requires.
+ */
+package com.iluwatar.bff.service;
diff --git a/backends-for-frontends/src/test/java/com/iluwatar/bff/AppTest.java b/backends-for-frontends/src/test/java/com/iluwatar/bff/AppTest.java
new file mode 100644
index 000000000000..d06d945c8c33
--- /dev/null
+++ b/backends-for-frontends/src/test/java/com/iluwatar/bff/AppTest.java
@@ -0,0 +1,41 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bff;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests that {@link App}'s demo entry point runs end-to-end without throwing, following the
+ * convention used throughout this repository for pattern demo apps.
+ */
+class AppTest {
+
+ @Test
+ void mainShouldRunWithoutException() {
+ assertDoesNotThrow(() -> App.main(new String[] {}));
+ }
+}
diff --git a/backends-for-frontends/src/test/java/com/iluwatar/bff/bff/DesktopBffTest.java b/backends-for-frontends/src/test/java/com/iluwatar/bff/bff/DesktopBffTest.java
new file mode 100644
index 000000000000..d4a2331b101b
--- /dev/null
+++ b/backends-for-frontends/src/test/java/com/iluwatar/bff/bff/DesktopBffTest.java
@@ -0,0 +1,77 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bff.bff;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.iluwatar.bff.model.Order;
+import com.iluwatar.bff.model.SupplierRecord;
+import com.iluwatar.bff.model.User;
+import com.iluwatar.bff.service.impl.InMemoryAuthService;
+import com.iluwatar.bff.service.impl.InMemoryOrderService;
+import com.iluwatar.bff.service.impl.InMemorySupplierService;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link DesktopBff}. */
+class DesktopBffTest {
+
+ private static final String USER_ID = "u-1";
+
+ @Test
+ void shouldAggregateOrdersAndSupplierStockIntoDesktopShape() {
+ var authService = new InMemoryAuthService(Map.of(USER_ID, new User(USER_ID, "Alice", "GOLD")));
+ var orderService =
+ new InMemoryOrderService(
+ Map.of(USER_ID, List.of(new Order("o-1", "Headphones", "DELIVERED"))));
+ var supplierService =
+ new InMemorySupplierService(
+ Map.of("Headphones", List.of(new SupplierRecord("p-1", "Acme Audio", 30))));
+
+ var bff = new DesktopBff(authService, orderService, supplierService);
+ var response = bff.getDashboard(USER_ID);
+
+ assertEquals("Welcome back, Alice", response.greeting());
+ assertEquals("GOLD", response.loyaltyTier());
+ assertTrue(response.orderStatuses().get(0).contains("Headphones"));
+ assertTrue(response.supplierStockSummaries().get(0).contains("Acme Audio"));
+ }
+
+ @Test
+ void shouldReturnNoSupplierSummariesWhenNoOrdersExist() {
+ var authService =
+ new InMemoryAuthService(Map.of(USER_ID, new User(USER_ID, "Carol", "STANDARD")));
+ var orderService = new InMemoryOrderService(Map.of(USER_ID, List.of()));
+ var supplierService = new InMemorySupplierService(Map.of());
+
+ var bff = new DesktopBff(authService, orderService, supplierService);
+ var response = bff.getDashboard(USER_ID);
+
+ assertEquals(0, response.orderStatuses().size());
+ assertEquals(0, response.supplierStockSummaries().size());
+ }
+}
diff --git a/backends-for-frontends/src/test/java/com/iluwatar/bff/bff/MobileBffTest.java b/backends-for-frontends/src/test/java/com/iluwatar/bff/bff/MobileBffTest.java
new file mode 100644
index 000000000000..f05a0be99b61
--- /dev/null
+++ b/backends-for-frontends/src/test/java/com/iluwatar/bff/bff/MobileBffTest.java
@@ -0,0 +1,83 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bff.bff;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.iluwatar.bff.model.CartItem;
+import com.iluwatar.bff.model.Order;
+import com.iluwatar.bff.model.Product;
+import com.iluwatar.bff.model.User;
+import com.iluwatar.bff.service.impl.InMemoryAuthService;
+import com.iluwatar.bff.service.impl.InMemoryCartService;
+import com.iluwatar.bff.service.impl.InMemoryOrderService;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link MobileBff}. */
+class MobileBffTest {
+
+ private static final String USER_ID = "u-1";
+
+ @Test
+ void shouldAggregateCartAndOrdersIntoMobileShape() {
+ var authService = new InMemoryAuthService(Map.of(USER_ID, new User(USER_ID, "Alice", "GOLD")));
+ var product = new Product("p-1", "Headphones", 50.0);
+ var cartService = new InMemoryCartService(Map.of(USER_ID, List.of(new CartItem(product, 2))));
+ var orderService =
+ new InMemoryOrderService(
+ Map.of(USER_ID, List.of(new Order("o-1", "Headphones", "DELIVERED"))));
+
+ var bff = new MobileBff(authService, cartService, orderService);
+ var response = bff.getDashboard(USER_ID);
+
+ assertEquals("Hi Alice!", response.greeting());
+ assertEquals(1, response.cartItemCount());
+ assertEquals(100.0, response.cartTotalUsd());
+ assertTrue(response.recentOrderSummaries().get(0).contains("Headphones"));
+ }
+
+ @Test
+ void shouldCapRecentOrderSummariesAtThree() {
+ var authService = new InMemoryAuthService(Map.of(USER_ID, new User(USER_ID, "Bob", "SILVER")));
+ var cartService = new InMemoryCartService(Map.of(USER_ID, List.of()));
+ var orderService =
+ new InMemoryOrderService(
+ Map.of(
+ USER_ID,
+ List.of(
+ new Order("o-1", "A", "DELIVERED"),
+ new Order("o-2", "B", "DELIVERED"),
+ new Order("o-3", "C", "DELIVERED"),
+ new Order("o-4", "D", "DELIVERED"))));
+
+ var bff = new MobileBff(authService, cartService, orderService);
+ var response = bff.getDashboard(USER_ID);
+
+ assertEquals(3, response.recentOrderSummaries().size());
+ }
+}
diff --git a/backends-for-frontends/src/test/java/com/iluwatar/bff/service/impl/InMemoryAuthServiceTest.java b/backends-for-frontends/src/test/java/com/iluwatar/bff/service/impl/InMemoryAuthServiceTest.java
new file mode 100644
index 000000000000..db4da0c0ba0d
--- /dev/null
+++ b/backends-for-frontends/src/test/java/com/iluwatar/bff/service/impl/InMemoryAuthServiceTest.java
@@ -0,0 +1,54 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bff.service.impl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import com.iluwatar.bff.model.User;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link InMemoryAuthService}. */
+class InMemoryAuthServiceTest {
+
+ private static final String USER_ID = "u-1";
+
+ @Test
+ void shouldReturnUserForKnownId() {
+ var expected = new User(USER_ID, "Alice", "GOLD");
+ var service = new InMemoryAuthService(Map.of(USER_ID, expected));
+
+ var actual = service.getUser(USER_ID);
+
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ void shouldThrowForUnknownId() {
+ var service = new InMemoryAuthService(Map.of(USER_ID, new User(USER_ID, "Alice", "GOLD")));
+
+ assertThrows(IllegalArgumentException.class, () -> service.getUser("unknown-id"));
+ }
+}
diff --git a/backends-for-frontends/src/test/java/com/iluwatar/bff/service/impl/InMemoryCartServiceTest.java b/backends-for-frontends/src/test/java/com/iluwatar/bff/service/impl/InMemoryCartServiceTest.java
new file mode 100644
index 000000000000..19e54841fa44
--- /dev/null
+++ b/backends-for-frontends/src/test/java/com/iluwatar/bff/service/impl/InMemoryCartServiceTest.java
@@ -0,0 +1,60 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bff.service.impl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.iluwatar.bff.model.CartItem;
+import com.iluwatar.bff.model.Product;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link InMemoryCartService}. */
+class InMemoryCartServiceTest {
+
+ private static final String USER_ID = "u-1";
+
+ @Test
+ void shouldReturnCartItemsForKnownUser() {
+ var product = new Product("p-1", "Headphones", 49.99);
+ var item = new CartItem(product, 2);
+ var service = new InMemoryCartService(Map.of(USER_ID, List.of(item)));
+
+ var cart = service.getCart(USER_ID);
+
+ assertEquals(1, cart.size());
+ assertEquals(item, cart.get(0));
+ }
+
+ @Test
+ void shouldReturnEmptyListForUnknownUser() {
+ var service = new InMemoryCartService(Map.of());
+
+ var cart = service.getCart("unknown-user");
+
+ assertTrue(cart.isEmpty());
+ }
+}
diff --git a/backends-for-frontends/src/test/java/com/iluwatar/bff/service/impl/InMemoryOrderServiceTest.java b/backends-for-frontends/src/test/java/com/iluwatar/bff/service/impl/InMemoryOrderServiceTest.java
new file mode 100644
index 000000000000..26b7d82be105
--- /dev/null
+++ b/backends-for-frontends/src/test/java/com/iluwatar/bff/service/impl/InMemoryOrderServiceTest.java
@@ -0,0 +1,58 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bff.service.impl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.iluwatar.bff.model.Order;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link InMemoryOrderService}. */
+class InMemoryOrderServiceTest {
+
+ private static final String USER_ID = "u-1";
+
+ @Test
+ void shouldReturnOrdersForKnownUser() {
+ var order = new Order("o-1", "Headphones", "DELIVERED");
+ var service = new InMemoryOrderService(Map.of(USER_ID, List.of(order)));
+
+ var orders = service.getOrders(USER_ID);
+
+ assertEquals(1, orders.size());
+ assertEquals(order, orders.get(0));
+ }
+
+ @Test
+ void shouldReturnEmptyListForUnknownUser() {
+ var service = new InMemoryOrderService(Map.of());
+
+ var orders = service.getOrders("unknown-user");
+
+ assertTrue(orders.isEmpty());
+ }
+}
diff --git a/backends-for-frontends/src/test/java/com/iluwatar/bff/service/impl/InMemorySupplierServiceTest.java b/backends-for-frontends/src/test/java/com/iluwatar/bff/service/impl/InMemorySupplierServiceTest.java
new file mode 100644
index 000000000000..ce5d39041033
--- /dev/null
+++ b/backends-for-frontends/src/test/java/com/iluwatar/bff/service/impl/InMemorySupplierServiceTest.java
@@ -0,0 +1,58 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bff.service.impl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.iluwatar.bff.model.SupplierRecord;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link InMemorySupplierService}. */
+class InMemorySupplierServiceTest {
+
+ private static final String PRODUCT_NAME = "Headphones";
+
+ @Test
+ void shouldReturnSupplierRecordsForKnownProductName() {
+ var record = new SupplierRecord("p-1", "Acme Audio", 30);
+ var service = new InMemorySupplierService(Map.of(PRODUCT_NAME, List.of(record)));
+
+ var records = service.getSupplierRecords(PRODUCT_NAME);
+
+ assertEquals(1, records.size());
+ assertEquals(record, records.get(0));
+ }
+
+ @Test
+ void shouldReturnEmptyListForUnknownProductName() {
+ var service = new InMemorySupplierService(Map.of());
+
+ var records = service.getSupplierRecords("unknown-product");
+
+ assertTrue(records.isEmpty());
+ }
+}
diff --git a/dynamic-proxy/pom.xml b/dynamic-proxy/pom.xml
index 7facbcf6d095..bf5250998c53 100644
--- a/dynamic-proxy/pom.xml
+++ b/dynamic-proxy/pom.xml
@@ -51,7 +51,7 @@
com.fasterxml.jackson.core
jackson-databind
- 2.18.3
+ 2.22.1
org.springframework
diff --git a/event-sourcing/pom.xml b/event-sourcing/pom.xml
index 27a72e5f5f65..eefacf7252d4 100644
--- a/event-sourcing/pom.xml
+++ b/event-sourcing/pom.xml
@@ -55,7 +55,7 @@
com.fasterxml.jackson.core
jackson-databind
- 2.18.3
+ 2.18.9
diff --git a/factory-method/README.md b/factory-method/README.md
index 4334b052353d..6a0046956211 100644
--- a/factory-method/README.md
+++ b/factory-method/README.md
@@ -103,13 +103,12 @@ Use the Factory Method Pattern in Java when:
## Real-World Applications of Factory Method Pattern in Java
-* [java.util.Calendar](http://docs.oracle.com/javase/8/docs/api/java/util/Calendar.html#getInstance--)
-* [java.util.ResourceBundle](http://docs.oracle.com/javase/8/docs/api/java/util/ResourceBundle.html#getBundle-java.lang.String-)
-* [java.text.NumberFormat](http://docs.oracle.com/javase/8/docs/api/java/text/NumberFormat.html#getInstance--)
-* [java.nio.charset.Charset](http://docs.oracle.com/javase/8/docs/api/java/nio/charset/Charset.html#forName-java.lang.String-)
-* [java.net.URLStreamHandlerFactory](http://docs.oracle.com/javase/8/docs/api/java/net/URLStreamHandlerFactory.html#createURLStreamHandler-java.lang.String-)
-* [java.util.EnumSet](https://docs.oracle.com/javase/8/docs/api/java/util/EnumSet.html#of-E-)
-* [javax.xml.bind.JAXBContext](https://docs.oracle.com/javase/8/docs/api/javax/xml/bind/JAXBContext.html#createMarshaller--)
+* [java.util.Calendar]()
+* [java.util.ResourceBundle]()
+* [java.text.NumberFormat]()
+* [java.nio.charset.Charset]()
+* [java.net.URLStreamHandlerFactory]()
+* [java.util.EnumSet]()
* Frameworks that run application components, configured dynamically at runtime.
## Benefits and Trade-offs of Factory Method Pattern
diff --git a/fallback/README.md b/fallback/README.md
new file mode 100644
index 000000000000..d6612e756444
--- /dev/null
+++ b/fallback/README.md
@@ -0,0 +1,114 @@
+---
+title: "Fallback Pattern in Java: Graceful Degradation in Microservices"
+shortTitle: Fallback
+description: "Learn about the Fallback pattern in Java design, which ensures microservice resilience and graceful system degradation when primary dependencies fail."
+category: Resilience
+language: en
+tag:
+ - Cloud distributed
+ - Fault tolerance
+ - Microservices
+---
+
+## Intent of Fallback Design Pattern
+
+The Fallback design pattern is a resiliency pattern used in microservices architecture to handle failures gracefully. It ensures that when a service is unavailable, fails, or times out, the system can continue to operate by providing an alternative response or executing a predefined fallback mechanism. This pattern enhances robustness and reliability by preventing cascading failures and improving the overall user experience.
+
+## Detailed Explanation of Fallback Pattern with Real-World Examples
+
+Real-world example
+
+> Consider a movie streaming application like Netflix. The home page loads personalized recommendations for the logged-in user. If the recommendation microservice goes offline or is too slow, the user shouldn't see a broken page. Instead, the system falls back to a cached list of globally popular movies. While the response is degraded (not personalized), the application remains functional, providing a seamless user experience.
+
+In plain words
+
+> Fallback ensures that if a primary service call fails, the application falls back to a backup strategy (e.g. cached response, default value, or simplified service) rather than raising an error and failing completely.
+
+Wikipedia says
+
+> A fallback is a contingency option to be taken if the preferred choice is unavailable. In software, fallback mechanisms are crucial for fault tolerance, allowing systems to degrade gracefully rather than crash.
+
+## Programmatic Example of Fallback Pattern in Java
+
+This Java example demonstrates how the Fallback pattern can manage service failures, integrate with a Circuit Breaker, and apply timeout limits.
+
+1. **Defining the Remote Service Interface**
+
+ The `RemoteService` interface represents any external dependency call.
+
+```java
+public interface RemoteService {
+ String execute() throws Exception;
+}
+```
+
+2. **Defining the Primary Service and Fallback Service**
+
+ The `PrimaryService` simulates our main external dependency which may suffer from errors or latency. The `FallbackService` returns a cached or degraded static response.
+
+```java
+// Primary Service simulating latency and errors
+var healthyPrimary = new PrimaryService("Healthy data from primary service", 10, false);
+var failingPrimary = new PrimaryService("Failing service", 0, true);
+var slowPrimary = new PrimaryService("Slow response from primary service", 500, false);
+
+// Fallback Service providing degraded response
+var fallback = new FallbackService("Fallback degraded/cached response");
+```
+
+3. **Monitoring Health with a Circuit Breaker**
+
+ A `SimpleCircuitBreaker` tracks the number of failures to trip the circuit to `OPEN`, bypassing the primary service immediately to avoid waiting for timeouts.
+
+```java
+// Trip after 2 failures; retry after 1 second
+var circuitBreaker = new SimpleCircuitBreaker(2, 1000);
+```
+
+4. **Executing Calls with the FallbackExecutor**
+
+ The `FallbackExecutor` uses virtual threads to execute the primary service call. It applies timeouts, handles exceptions, records failures to the circuit breaker, and falls back to the fallback service as needed.
+
+```java
+try (var executor = new FallbackExecutor()) {
+ // Scenario 1: Healthy primary service call
+ String response1 = executor.execute(healthyPrimary, fallback, circuitBreaker, 100);
+ LOGGER.info("Response: {}", response1); // Healthy data from primary service
+
+ // Scenario 2: Failing service call triggers fallback
+ String response2 = executor.execute(failingPrimary, fallback, circuitBreaker, 100);
+ LOGGER.info("Response: {}", response2); // Fallback degraded/cached response
+}
+```
+
+## When to Use the Fallback Pattern in Java
+
+The Fallback pattern is applicable:
+
+* In microservices architectures where dependencies are called over the network and are prone to network partitions, timeouts, and outages.
+* When returning a default, empty, or cached value is preferable to failing the entire request.
+* In user-facing systems where maintaining a working UI (even with degraded features) is critical for user satisfaction.
+
+## Real-World Applications of Fallback Pattern in Java
+
+* [Resilience4j Fallback mechanism](https://resilience4j.readme.io/docs/fallback)
+* [Netflix Hystrix Fallback](https://github.com/Netflix/Hystrix/wiki/How-To-Use#Fallback)
+* Spring Cloud Circuit Breaker integrations
+
+## Benefits and Trade-offs of Fallback Pattern
+
+Benefits:
+
+* **Graceful Degradation**: Improves user experience by returning partial/cached data instead of errors.
+* **Cascading Failure Prevention**: Avoids blocking threads waiting on hung services.
+* **Fault Tolerance**: Improves system uptime and reliability.
+
+Trade-Offs:
+
+* **Stale Data**: Fallback cached responses may present out-of-date information to the user.
+* **Increased Complexity**: Requires writing alternative execution flows and testing fallback scenarios.
+
+## Related Patterns
+
+- [Circuit Breaker](https://github.com/iluwatar/java-design-patterns/tree/master/circuit-breaker): Restricts calls to failing services. Often wraps the primary service before fallback is triggered.
+- [Retry Pattern](https://github.com/iluwatar/java-design-patterns/tree/master/retry): Retries failed calls before triggering the fallback.
diff --git a/fallback/pom.xml b/fallback/pom.xml
new file mode 100644
index 000000000000..361aea79079b
--- /dev/null
+++ b/fallback/pom.xml
@@ -0,0 +1,70 @@
+
+
+
+ 4.0.0
+
+ com.iluwatar
+ java-design-patterns
+ 1.26.0-SNAPSHOT
+
+ fallback
+
+
+ org.slf4j
+ slf4j-api
+
+
+ ch.qos.logback
+ logback-classic
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+
+
+
+
+
+ com.iluwatar.fallback.App
+
+
+
+
+
+
+
+
+
diff --git a/fallback/src/main/java/com/iluwatar/fallback/App.java b/fallback/src/main/java/com/iluwatar/fallback/App.java
new file mode 100644
index 000000000000..6e51acc5e5a0
--- /dev/null
+++ b/fallback/src/main/java/com/iluwatar/fallback/App.java
@@ -0,0 +1,99 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.fallback;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The Fallback design pattern is a resiliency pattern used in microservices architecture to handle
+ * failures gracefully. When a service is unavailable, fails, or times out, the system responds with
+ * a pre-configured fallback mechanism (like cached data or a simplified response).
+ *
+ * This App demonstrates: 1. Healthy calls returning standard responses. 2. Failing calls
+ * (throwing exception) falling back to a fallback handler. 3. Latent calls (timing out) falling
+ * back. 4. Circuit Breaker tripping and immediately fast-failing to the fallback handler. 5.
+ * Recovery after a retry duration, returning the system to a healthy CLOSED state.
+ */
+public class App {
+ private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
+
+ /**
+ * Main entry point for the application.
+ *
+ * @param args Command line arguments (not used)
+ */
+ public static void main(String[] args) {
+ try (var executor = new FallbackExecutor()) {
+ var healthyPrimary = new PrimaryService("Healthy data from primary service", 10, false);
+ var failingPrimary = new PrimaryService("Failing service", 0, true);
+ var slowPrimary = new PrimaryService("Slow response from primary service", 500, false);
+ var fallback = new FallbackService("Fallback degraded/cached response");
+
+ // Failure threshold is 2 failures, retry time period is 1 second (1000ms)
+ var circuitBreaker = new SimpleCircuitBreaker(2, 1000);
+
+ // Scenario 1: Healthy primary service call
+ LOGGER.info("Scenario 1: Executing request to healthy primary service...");
+ String response1 = executor.execute(healthyPrimary, fallback, circuitBreaker, 100);
+ LOGGER.info("Response received: {}", response1);
+ LOGGER.info("Circuit Breaker State: {}\n", circuitBreaker.getState());
+
+ // Scenario 2: Failing primary service call (fails and increments failure count to 1)
+ LOGGER.info("Scenario 2: Executing request to failing primary service (throws exception)...");
+ String response2 = executor.execute(failingPrimary, fallback, circuitBreaker, 100);
+ LOGGER.info("Response received: {}", response2);
+ LOGGER.info("Circuit Breaker State: {}\n", circuitBreaker.getState());
+
+ // Scenario 3: Slow primary service call (times out and increments failure count to 2,
+ // tripping breaker)
+ LOGGER.info("Scenario 3: Executing request to slow primary service (triggers timeout)...");
+ String response3 = executor.execute(slowPrimary, fallback, circuitBreaker, 100);
+ LOGGER.info("Response received: {}", response3);
+ LOGGER.info("Circuit Breaker State: {}\n", circuitBreaker.getState());
+
+ // Scenario 4: Fast failing when circuit is OPEN
+ LOGGER.info("Scenario 4: Executing request while Circuit Breaker is OPEN...");
+ String response4 = executor.execute(healthyPrimary, fallback, circuitBreaker, 100);
+ LOGGER.info("Response received: {}", response4);
+ LOGGER.info("Circuit Breaker State: {}\n", circuitBreaker.getState());
+
+ // Scenario 5: Recovery from OPEN state
+ LOGGER.info("Scenario 5: Waiting for retry period to elapse...");
+ try {
+ Thread.sleep(1100); // Wait longer than retryTimePeriodMs (1000ms)
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ LOGGER.info(
+ "Circuit Breaker State (should be HALF_OPEN on next check): {}",
+ circuitBreaker.getState());
+ LOGGER.info("Executing request to healthy primary service to reset breaker...");
+ String response5 = executor.execute(healthyPrimary, fallback, circuitBreaker, 100);
+ LOGGER.info("Response received: {}", response5);
+ LOGGER.info("Circuit Breaker State (should be CLOSED): {}\n", circuitBreaker.getState());
+ }
+ }
+}
diff --git a/fallback/src/main/java/com/iluwatar/fallback/FallbackExecutor.java b/fallback/src/main/java/com/iluwatar/fallback/FallbackExecutor.java
new file mode 100644
index 000000000000..01bf3a204d01
--- /dev/null
+++ b/fallback/src/main/java/com/iluwatar/fallback/FallbackExecutor.java
@@ -0,0 +1,110 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.fallback;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Orchestrates primary service execution with timeouts, circuit breaker health checks, and fallback
+ * execution logic. Implements AutoCloseable to ensure thread pools are closed cleanly.
+ */
+public class FallbackExecutor implements AutoCloseable {
+ private static final Logger LOGGER = LoggerFactory.getLogger(FallbackExecutor.class);
+ private final ExecutorService executorService;
+
+ /** Constructor for FallbackExecutor. Initializes a virtual thread per task executor. */
+ public FallbackExecutor() {
+ this.executorService = Executors.newVirtualThreadPerTaskExecutor();
+ }
+
+ /**
+ * Executes the primary service call. If it fails, times out, or the circuit breaker is open, it
+ * falls back to the fallback service.
+ *
+ * @param primary the primary service call to execute
+ * @param fallback the fallback service to call when primary fails or is bypassed
+ * @param circuitBreaker the circuit breaker monitoring service health
+ * @param timeoutMs timeout limit for the primary service in milliseconds
+ * @return response string
+ */
+ public String execute(
+ RemoteService primary,
+ RemoteService fallback,
+ SimpleCircuitBreaker circuitBreaker,
+ long timeoutMs) {
+
+ // 1. Check Circuit Breaker
+ if (circuitBreaker.getState() == SimpleCircuitBreaker.State.OPEN) {
+ LOGGER.warn("Circuit is OPEN. Fast-failing and calling fallback service.");
+ try {
+ return fallback.execute();
+ } catch (Exception ex) {
+ LOGGER.error("Fallback service execution failed: {}", ex.getMessage());
+ return "Fallback Error";
+ }
+ }
+
+ // 2. Attempt service call with timeout
+ Callable task = primary::execute;
+ Future future = executorService.submit(task);
+
+ try {
+ String result = future.get(timeoutMs, TimeUnit.MILLISECONDS);
+ circuitBreaker.recordSuccess();
+ return result;
+ } catch (TimeoutException e) {
+ LOGGER.error("Service call timed out. Triggering fallback.");
+ future.cancel(true); // Interrupt / cancel the task
+ circuitBreaker.recordFailure();
+ try {
+ return fallback.execute();
+ } catch (Exception ex) {
+ LOGGER.error("Fallback service execution failed: {}", ex.getMessage());
+ return "Fallback Error";
+ }
+ } catch (Exception e) {
+ LOGGER.error("Service call failed with exception: {}. Triggering fallback.", e.getMessage());
+ circuitBreaker.recordFailure();
+ try {
+ return fallback.execute();
+ } catch (Exception ex) {
+ LOGGER.error("Fallback service execution failed: {}", ex.getMessage());
+ return "Fallback Error";
+ }
+ }
+ }
+
+ @Override
+ public void close() {
+ executorService.shutdown();
+ }
+}
diff --git a/fallback/src/main/java/com/iluwatar/fallback/FallbackService.java b/fallback/src/main/java/com/iluwatar/fallback/FallbackService.java
new file mode 100644
index 000000000000..c84925f20606
--- /dev/null
+++ b/fallback/src/main/java/com/iluwatar/fallback/FallbackService.java
@@ -0,0 +1,47 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.fallback;
+
+/**
+ * A concrete implementation of the remote service representing the fallback handler. It is invoked
+ * when the primary service fails, returns a cached response or degrades gracefully.
+ */
+public class FallbackService implements RemoteService {
+ private final String fallbackResponse;
+
+ /**
+ * Constructor for FallbackService.
+ *
+ * @param fallbackResponse the fallback response to return
+ */
+ public FallbackService(String fallbackResponse) {
+ this.fallbackResponse = fallbackResponse;
+ }
+
+ @Override
+ public String execute() {
+ return fallbackResponse;
+ }
+}
diff --git a/fallback/src/main/java/com/iluwatar/fallback/PrimaryService.java b/fallback/src/main/java/com/iluwatar/fallback/PrimaryService.java
new file mode 100644
index 000000000000..55cdb2644e93
--- /dev/null
+++ b/fallback/src/main/java/com/iluwatar/fallback/PrimaryService.java
@@ -0,0 +1,59 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.fallback;
+
+/**
+ * A concrete implementation of the remote service representing the primary service. It can be
+ * configured to simulate latency and errors to test resilience features.
+ */
+public class PrimaryService implements RemoteService {
+ private final long latencyMs;
+ private final String response;
+ private final boolean shouldThrowException;
+
+ /**
+ * Constructor for PrimaryService.
+ *
+ * @param response the successful response to return
+ * @param latencyMs simulated latency in milliseconds
+ * @param shouldThrowException if true, the service will throw an exception
+ */
+ public PrimaryService(String response, long latencyMs, boolean shouldThrowException) {
+ this.latencyMs = latencyMs;
+ this.response = response;
+ this.shouldThrowException = shouldThrowException;
+ }
+
+ @Override
+ public String execute() throws Exception {
+ if (shouldThrowException) {
+ throw new RuntimeException("Primary service failed!");
+ }
+ if (latencyMs > 0) {
+ Thread.sleep(latencyMs);
+ }
+ return response;
+ }
+}
diff --git a/fallback/src/main/java/com/iluwatar/fallback/RemoteService.java b/fallback/src/main/java/com/iluwatar/fallback/RemoteService.java
new file mode 100644
index 000000000000..7e84a7fdbe21
--- /dev/null
+++ b/fallback/src/main/java/com/iluwatar/fallback/RemoteService.java
@@ -0,0 +1,36 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.fallback;
+
+/** Representation of a service (e.g. a microservice client) that might fail. */
+public interface RemoteService {
+ /**
+ * Executes the service logic.
+ *
+ * @return the service response
+ * @throws Exception if service call fails or is interrupted
+ */
+ String execute() throws Exception;
+}
diff --git a/fallback/src/main/java/com/iluwatar/fallback/SimpleCircuitBreaker.java b/fallback/src/main/java/com/iluwatar/fallback/SimpleCircuitBreaker.java
new file mode 100644
index 000000000000..185167820364
--- /dev/null
+++ b/fallback/src/main/java/com/iluwatar/fallback/SimpleCircuitBreaker.java
@@ -0,0 +1,96 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.fallback;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** A simplified circuit breaker implementation for tracking remote service health. */
+public class SimpleCircuitBreaker {
+ private static final Logger LOGGER = LoggerFactory.getLogger(SimpleCircuitBreaker.class);
+
+ private final int failureThreshold;
+ private final long retryTimePeriodMs;
+ private int failureCount = 0;
+ private long lastFailureTime = 0;
+ private State state = State.CLOSED;
+
+ /** The state of the circuit breaker. */
+ public enum State {
+ CLOSED,
+ OPEN,
+ HALF_OPEN
+ }
+
+ /**
+ * Constructor for SimpleCircuitBreaker.
+ *
+ * @param failureThreshold consecutive failure count threshold to trip the breaker
+ * @param retryTimePeriodMs time duration to wait in OPEN state before trying again (HALF_OPEN)
+ */
+ public SimpleCircuitBreaker(int failureThreshold, long retryTimePeriodMs) {
+ this.failureThreshold = failureThreshold;
+ this.retryTimePeriodMs = retryTimePeriodMs;
+ }
+
+ /**
+ * Get the current state of the circuit breaker after evaluating transitions.
+ *
+ * @return current state
+ */
+ public synchronized State getState() {
+ evaluateState();
+ return state;
+ }
+
+ private void evaluateState() {
+ if (state == State.OPEN) {
+ if (System.currentTimeMillis() - lastFailureTime > retryTimePeriodMs) {
+ state = State.HALF_OPEN;
+ LOGGER.info("Circuit Breaker transitioned to HALF_OPEN");
+ }
+ }
+ }
+
+ /** Records a successful operation, resetting the failure counter and closing the circuit. */
+ public synchronized void recordSuccess() {
+ failureCount = 0;
+ state = State.CLOSED;
+ LOGGER.info("Circuit Breaker transitioned to CLOSED (success recorded)");
+ }
+
+ /** Records a failure, potentially tripping the circuit to OPEN if threshold is met. */
+ public synchronized void recordFailure() {
+ failureCount++;
+ lastFailureTime = System.currentTimeMillis();
+ if (state == State.CLOSED && failureCount >= failureThreshold) {
+ state = State.OPEN;
+ LOGGER.warn("Circuit Breaker transitioned to OPEN (failure threshold reached)");
+ } else if (state == State.HALF_OPEN) {
+ state = State.OPEN;
+ LOGGER.warn("Circuit Breaker transitioned to OPEN (failed during HALF_OPEN)");
+ }
+ }
+}
diff --git a/fallback/src/test/java/com/iluwatar/fallback/AppTest.java b/fallback/src/test/java/com/iluwatar/fallback/AppTest.java
new file mode 100644
index 000000000000..86ffb6f363be
--- /dev/null
+++ b/fallback/src/test/java/com/iluwatar/fallback/AppTest.java
@@ -0,0 +1,37 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.fallback;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+import org.junit.jupiter.api.Test;
+
+/** Test verifying that App main method runs without throwing exceptions. */
+class AppTest {
+ @Test
+ void testMain() {
+ assertDoesNotThrow(() -> App.main(new String[] {}));
+ }
+}
diff --git a/fallback/src/test/java/com/iluwatar/fallback/FallbackPatternTest.java b/fallback/src/test/java/com/iluwatar/fallback/FallbackPatternTest.java
new file mode 100644
index 000000000000..f338c1c8c5c1
--- /dev/null
+++ b/fallback/src/test/java/com/iluwatar/fallback/FallbackPatternTest.java
@@ -0,0 +1,122 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.fallback;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+/** Unit and integration tests for the Fallback design pattern. */
+class FallbackPatternTest {
+
+ @Test
+ void testHealthyServiceCall() {
+ try (var executor = new FallbackExecutor()) {
+ var primary = new PrimaryService("Primary Response", 0, false);
+ var fallback = new FallbackService("Fallback Response");
+ var circuitBreaker = new SimpleCircuitBreaker(2, 1000);
+
+ String response = executor.execute(primary, fallback, circuitBreaker, 100);
+ assertEquals("Primary Response", response);
+ assertEquals(SimpleCircuitBreaker.State.CLOSED, circuitBreaker.getState());
+ }
+ }
+
+ @Test
+ void testFailingServiceCall() {
+ try (var executor = new FallbackExecutor()) {
+ var primary = new PrimaryService("Primary Response", 0, true);
+ var fallback = new FallbackService("Fallback Response");
+ var circuitBreaker = new SimpleCircuitBreaker(2, 1000);
+
+ // First failure: should call fallback, state remains CLOSED since threshold is 2
+ String response1 = executor.execute(primary, fallback, circuitBreaker, 100);
+ assertEquals("Fallback Response", response1);
+ assertEquals(SimpleCircuitBreaker.State.CLOSED, circuitBreaker.getState());
+
+ // Second failure: should call fallback, state becomes OPEN
+ String response2 = executor.execute(primary, fallback, circuitBreaker, 100);
+ assertEquals("Fallback Response", response2);
+ assertEquals(SimpleCircuitBreaker.State.OPEN, circuitBreaker.getState());
+ }
+ }
+
+ @Test
+ void testTimeoutServiceCall() {
+ try (var executor = new FallbackExecutor()) {
+ // Primary takes 300ms, timeout limit is 50ms
+ var primary = new PrimaryService("Primary Response", 300, false);
+ var fallback = new FallbackService("Fallback Response");
+ var circuitBreaker = new SimpleCircuitBreaker(1, 1000);
+
+ String response = executor.execute(primary, fallback, circuitBreaker, 50);
+ assertEquals("Fallback Response", response);
+ assertEquals(SimpleCircuitBreaker.State.OPEN, circuitBreaker.getState());
+ }
+ }
+
+ @Test
+ void testCircuitBreakerOpenFastFail() {
+ try (var executor = new FallbackExecutor()) {
+ // Configure primary service to throw exception if called
+ var primary = new PrimaryService("Primary Response", 0, true);
+ var fallback = new FallbackService("Fallback Response");
+ var circuitBreaker = new SimpleCircuitBreaker(1, 1000);
+
+ // Force open by registering a failure
+ circuitBreaker.recordFailure();
+ assertEquals(SimpleCircuitBreaker.State.OPEN, circuitBreaker.getState());
+
+ // Now call execute. It should short-circuit and not call the failing primary (fast-fail).
+ String response = executor.execute(primary, fallback, circuitBreaker, 100);
+ assertEquals("Fallback Response", response);
+ }
+ }
+
+ @Test
+ void testCircuitBreakerRecovery() throws InterruptedException {
+ try (var executor = new FallbackExecutor()) {
+ var primary = new PrimaryService("Primary Response", 0, false);
+ var fallback = new FallbackService("Fallback Response");
+ // Failure threshold = 1, retry period = 100ms
+ var circuitBreaker = new SimpleCircuitBreaker(1, 100);
+
+ // Trip the breaker
+ circuitBreaker.recordFailure();
+ assertEquals(SimpleCircuitBreaker.State.OPEN, circuitBreaker.getState());
+
+ // Wait 150ms to exceed retry period
+ Thread.sleep(150);
+
+ // State should evaluate to HALF_OPEN
+ assertEquals(SimpleCircuitBreaker.State.HALF_OPEN, circuitBreaker.getState());
+
+ // Execute. Healthy primary service succeeds, state transitions to CLOSED
+ String response = executor.execute(primary, fallback, circuitBreaker, 100);
+ assertEquals("Primary Response", response);
+ assertEquals(SimpleCircuitBreaker.State.CLOSED, circuitBreaker.getState());
+ }
+ }
+}
diff --git a/microservices-messaging/README.md b/microservices-messaging/README.md
new file mode 100644
index 000000000000..3ca95350e2a8
--- /dev/null
+++ b/microservices-messaging/README.md
@@ -0,0 +1,247 @@
+---
+title: "Microservices Messaging Pattern in Java: Enabling Asynchronous Communication Between Services"
+shortTitle: Microservices Messaging
+description: "Learn about the Microservices Messaging pattern, a method for enabling asynchronous communication between services through message brokers to enhance decoupling, scalability, and fault tolerance in distributed systems."
+category: Integration
+language: en
+tag:
+ - API design
+ - Asynchronous
+ - Cloud distributed
+ - Decoupling
+ - Enterprise patterns
+ - Event-driven
+ - Messaging
+ - Microservices
+ - Scalability
+---
+## Also known as
+
+* Asynchronous Messaging
+* Event-Driven Communication
+* Message-Oriented Middleware (MOM)
+
+## Intent of Microservices Messaging Design Pattern
+
+The Microservices Messaging pattern enables asynchronous communication between microservices through message passing, allowing for better decoupling, scalability, and fault tolerance. Services communicate by exchanging messages over messaging channels managed by a message broker.
+
+## Detailed Explanation of Microservices Messaging Pattern with Real-World Examples
+
+Real-world example
+
+> Imagine an e-commerce platform where a customer places an order. The Order Service publishes an "Order Created" message to a message broker. Multiple services listen to this message: the Inventory Service updates stock levels, the Payment Service processes payment, and the Notification Service sends confirmation emails. Each service operates independently, processing messages at its own pace without blocking others. If the Payment Service is temporarily down, the message broker holds the message until it recovers, ensuring no data is lost.
+
+In plain words
+
+> The Microservices Messaging pattern allows services to communicate asynchronously through a message broker, enabling them to work independently without waiting for each other.
+
+Wikipedia says
+
+> Message-oriented middleware is software or hardware infrastructure supporting sending and receiving messages between distributed systems. MOM allows application modules to be distributed over heterogeneous platforms and reduces the complexity of developing applications that span multiple operating systems and network protocols.
+
+Flowchart
+
+
+
+
+
+## Programmatic Example of Microservices Messaging Pattern in Java
+
+
+The Microservices Messaging pattern demonstrates how services communicate through a message broker without direct coupling. In this example, we show an order processing system where services exchange messages asynchronously.
+
+The `Message` class represents the data exchanged between services.
+
+```java
+public class Message {
+ private final String id;
+ private final String content;
+ private final LocalDateTime timestamp;
+
+ public Message(String content) {
+ this.id = UUID.randomUUID().toString();
+ this.content = content;
+ this.timestamp = LocalDateTime.now();
+ }
+
+ // Getters
+}
+```
+
+The `MessageBroker` acts as the intermediary that routes messages between producers and consumers.
+
+```java
+public class MessageBroker {
+ private final Map subscribers = new ConcurrentHashMap<>();
+
+ public void subscribe(String topic, Consumer handler) {
+ subscribers.computeIfAbsent(topic, k -> new ArrayList<>()).add(handler);
+ }
+
+ public void publish(String topic, Message message) {
+ List handlers = subscribers.get(topic);
+ if (handlers != null) {
+ handlers.forEach(handler -> handler.accept(message));
+ }
+ }
+}
+```
+
+The `OrderService` is a message producer that publishes order messages.
+
+```java
+public class OrderService {
+ private static final Logger LOGGER = LoggerFactory.getLogger(OrderService.class);
+ private final MessageBroker broker;
+
+ public OrderService(MessageBroker broker) {
+ this.broker = broker;
+ }
+
+ public void createOrder(String orderId) {
+ Message message = new Message("Order Created: " + orderId);
+ broker.publish("order-topic", message);
+ LOGGER.info("Published order message: {}", orderId);
+ }
+}
+```
+
+The `InventoryService` is a message consumer that processes inventory updates.
+
+```java
+public class InventoryService {
+ private static final Logger LOGGER = LoggerFactory.getLogger(InventoryService.class);
+
+ public void handleMessage(Message message) {
+ LOGGER.info("Inventory Service received: {}", message.getContent());
+ LOGGER.info("Updating inventory...");
+ }
+}
+```
+
+The `PaymentService` handles payment processing messages.
+
+```java
+public class PaymentService {
+ private static final Logger LOGGER = LoggerFactory.getLogger(PaymentService.class);
+
+ public void handleMessage(Message message) {
+ LOGGER.info("Payment Service received: {}", message.getContent());
+ LOGGER.info("Processing payment...");
+ }
+}
+```
+
+The `main` application demonstrates the messaging pattern in action.
+
+```java
+public class App {
+ private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
+
+ public static void main(String[] args) throws InterruptedException {
+ final MessageBroker broker = new MessageBroker();
+
+ final InventoryService inventoryService = new InventoryService();
+ final PaymentService paymentService = new PaymentService();
+
+ broker.subscribe("order-topic", inventoryService::handleMessage);
+ broker.subscribe("order-topic", paymentService::handleMessage);
+
+ final OrderService orderService = new OrderService(broker);
+
+ orderService.createOrder("ORDER-123");
+
+ Thread.sleep(1000);
+ }
+}
+```
+
+Console output:
+
+```
+Published order message: ORDER-123
+Inventory Service received: Order Created: ORDER-123
+Updating inventory...
+Payment Service received: Order Created: ORDER-123
+Processing payment...
+```
+
+Sequence Diagram
+
+
+
+## How to Run the Application
+
+### Option 1: Automated Script (Recommended)
+
+Run the helper script from the module directory, which automatically starts Kafka via Docker Compose (if Docker is installed and Kafka is not already running) and launches the application:
+
+* **Windows (PowerShell)**:
+ ```powershell
+ powershell -ExecutionPolicy Bypass -File .\run-app.ps1
+ ```
+* **Linux / macOS**:
+ ```bash
+ ./run-app.sh
+ ```
+
+### Option 2: Docker Compose
+
+Start the Kafka container manually via Docker Compose and run the application:
+
+```bash
+# Start Kafka container on port 9092
+docker compose up -d
+
+# Run the application
+../mvnw compile exec:java -Dexec.mainClass="com.iluwatar.messaging.App"
+
+# Stop Kafka container when finished
+docker compose down
+```
+
+## When to Use the Microservices Messaging Pattern in Java
+
+* When services need to communicate without blocking each other.
+* In systems requiring loose coupling between components.
+* For event-driven architectures where multiple services react to events.
+* When you need to handle traffic spikes by buffering messages.
+* In distributed systems where services may be temporarily unavailable.
+
+## Real-World Applications of Microservices Messaging Pattern in Java
+
+* Java applications using Apache Kafka, RabbitMQ, or ActiveMQ for service communication.
+* E-commerce platforms for order processing and inventory management.
+* Financial services for transaction processing and notifications.
+* IoT systems for sensor data processing and event handling.
+
+## Benefits and Trade-offs of Microservices Messaging Pattern
+
+* Services are loosely coupled and can be developed and deployed independently.
+* Message buffering improves system resilience when services are temporarily unavailable.
+* Supports multiple communication patterns like publish/subscribe and request/reply.
+* Enhances scalability by allowing parallel message processing.
+* Natural support for event-driven architectures.
+
+Trade-offs:
+
+* Introduces additional complexity with the message broker infrastructure.
+* Requires high availability setup for the message broker.
+* Eventual consistency instead of immediate consistency.
+* Debugging asynchronous flows is more complex than synchronous calls.
+* Need to handle message duplication and ensure idempotent consumers.
+
+## Related Java Design Patterns
+
+* [Saga Pattern](https://java-design-patterns.com/patterns/saga/): Uses messaging to coordinate distributed transactions.
+* [CQRS Pattern](https://java-design-patterns.com/patterns/cqrs/): Often uses messaging to separate read and write operations.
+* [Event Sourcing](https://java-design-patterns.com/patterns/event-sourcing/): Stores state changes as messages.
+* [API Gateway](https://java-design-patterns.com/patterns/microservices-api-gateway/): Complements messaging for synchronous requests.
+
+## References and Credits
+
+* [Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions](https://amzn.to/3vLKqET)
+* [Microservices Patterns: With examples in Java](https://amzn.to/3UyWD5O)
+* [Building Event-Driven Microservices: Leveraging Organizational Data at Scale](https://amzn.to/3PihS9R)
+* [Pattern: Messaging (microservices.io)](https://microservices.io/patterns/communication-style/messaging.html)
+* [Apache Kafka Documentation](https://kafka.apache.org/documentation/)
\ No newline at end of file
diff --git a/microservices-messaging/docker-compose.yml b/microservices-messaging/docker-compose.yml
new file mode 100644
index 000000000000..b77ee549cd51
--- /dev/null
+++ b/microservices-messaging/docker-compose.yml
@@ -0,0 +1,53 @@
+#
+# This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+#
+# The MIT License
+# Copyright © 2014-2022 Ilkka Seppälä
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+version: '3.8'
+
+services:
+ kafka:
+ image: confluentinc/cp-kafka:7.5.0
+ container_name: kafka-messaging-demo
+ ports:
+ - "9092:9092"
+ environment:
+ KAFKA_NODE_ID: 1
+ KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT'
+ KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092'
+ KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
+ KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
+ KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
+ KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
+ KAFKA_PROCESS_ROLES: 'broker,controller'
+ KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka:29093'
+ KAFKA_LISTENERS: 'PLAINTEXT://kafka:29092,CONTROLLER://kafka:29093,PLAINTEXT_HOST://0.0.0.0:9092'
+ KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT'
+ KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
+ KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'
+ CLUSTER_ID: 'MkU3OEVBNTcwNTJENDM2Qk'
+ healthcheck:
+ test: ["CMD-SHELL", "kafka-topics --bootstrap-server localhost:9092 --list"]
+ interval: 5s
+ timeout: 10s
+ retries: 5
diff --git a/microservices-messaging/etc/microservices-messaging-flowchart.png b/microservices-messaging/etc/microservices-messaging-flowchart.png
new file mode 100644
index 000000000000..dd26a2e2ce4a
Binary files /dev/null and b/microservices-messaging/etc/microservices-messaging-flowchart.png differ
diff --git a/microservices-messaging/etc/microservices-messaging-sequence-diagram.png b/microservices-messaging/etc/microservices-messaging-sequence-diagram.png
new file mode 100644
index 000000000000..d22be5acbb5d
Binary files /dev/null and b/microservices-messaging/etc/microservices-messaging-sequence-diagram.png differ
diff --git a/microservices-messaging/pom.xml b/microservices-messaging/pom.xml
new file mode 100644
index 000000000000..8fb9d59c4f74
--- /dev/null
+++ b/microservices-messaging/pom.xml
@@ -0,0 +1,118 @@
+
+
+
+ 4.0.0
+
+
+ com.iluwatar
+ java-design-patterns
+ 1.26.0-SNAPSHOT
+
+
+ microservices-messaging
+ 1.26.0-SNAPSHOT
+
+
+ 3.6.1
+
+
+
+
+
+ org.apache.kafka
+ kafka-clients
+ ${kafka.version}
+
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+ provided
+
+
+
+
+ org.slf4j
+ slf4j-api
+
+
+ ch.qos.logback
+ logback-classic
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.16.1
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+ 2.19.2
+ compile
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+
+
+
+
+
+ com.iluwatar.messaging.App
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/microservices-messaging/run-app.ps1 b/microservices-messaging/run-app.ps1
new file mode 100644
index 000000000000..6fd4da50dfbe
--- /dev/null
+++ b/microservices-messaging/run-app.ps1
@@ -0,0 +1,89 @@
+#
+# This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+#
+# The MIT License
+# Copyright © 2014-2022 Ilkka Seppälä
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+# PowerShell script to start Kafka container (if Docker is available) and run the Microservices Messaging App
+
+$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
+Set-Location $ScriptDir
+
+Write-Host "======================================================" -ForegroundColor Cyan
+Write-Host " Starting Microservices Messaging Pattern Application" -ForegroundColor Cyan
+Write-Host "======================================================" -ForegroundColor Cyan
+
+# Check if Kafka is already running on port 9092
+$portActive = $false
+try {
+ $socket = New-Object System.Net.Sockets.TcpClient("localhost", 9092)
+ if ($socket.Connected) {
+ $portActive = $true
+ $socket.Close()
+ }
+} catch {
+ $portActive = $false
+}
+
+if ($portActive) {
+ Write-Host "[INFO] Kafka is already running on port 9092." -ForegroundColor Green
+} else {
+ # Check if Docker is available, or add Docker Desktop to PATH
+ if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
+ $dockerPath = "$env:LOCALAPPDATA\Programs\DockerDesktop\resources\bin"
+ if (Test-Path $dockerPath) {
+ $env:PATH += ";$dockerPath"
+ }
+ }
+ $dockerCmd = Get-Command docker -ErrorAction SilentlyContinue
+ if ($dockerCmd) {
+ Write-Host "[INFO] Starting Kafka container via Docker Compose..." -ForegroundColor Yellow
+ docker compose up -d
+
+ Write-Host "[INFO] Waiting for Kafka to become ready on port 9092..." -ForegroundColor Yellow
+ $retryCount = 0
+ while (-not $portActive -and $retryCount -lt 20) {
+ Start-Sleep -Seconds 2
+ try {
+ $socket = New-Object System.Net.Sockets.TcpClient("localhost", 9092)
+ if ($socket.Connected) {
+ $portActive = $true
+ $socket.Close()
+ }
+ } catch {
+ $retryCount++
+ }
+ }
+
+ if ($portActive) {
+ Write-Host "[INFO] Kafka container started successfully!" -ForegroundColor Green
+ } else {
+ Write-Host "[WARNING] Kafka did not become ready on port 9092 within timeout." -ForegroundColor Red
+ }
+ } else {
+ Write-Host "[WARNING] Docker is not installed or not in PATH." -ForegroundColor Yellow
+ Write-Host "[INFO] Please ensure Kafka is running locally on localhost:9092 before running the app." -ForegroundColor Yellow
+ }
+}
+
+Write-Host "[INFO] Compiling and running App.java..." -ForegroundColor Cyan
+& "..\mvnw.cmd" compile exec:java "-Dexec.mainClass=com.iluwatar.messaging.App"
diff --git a/microservices-messaging/run-app.sh b/microservices-messaging/run-app.sh
new file mode 100644
index 000000000000..8c31c185190e
--- /dev/null
+++ b/microservices-messaging/run-app.sh
@@ -0,0 +1,62 @@
+#!/usr/bin/env bash
+#
+# This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+#
+# The MIT License
+# Copyright © 2014-2022 Ilkka Seppälä
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+# Shell script to start Kafka container (if Docker is available) and run the Microservices Messaging App
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+cd "$SCRIPT_DIR"
+
+echo "======================================================"
+echo " Starting Microservices Messaging Pattern Application"
+echo "======================================================"
+
+if nc -z localhost 9092 2>/dev/null || (echo > /dev/tcp/localhost/9092) 2>/dev/null; then
+ echo "[INFO] Kafka is already running on port 9092."
+else
+ if command -v docker &> /dev/null; then
+ echo "[INFO] Starting Kafka container via Docker Compose..."
+ docker compose up -d
+
+ echo "[INFO] Waiting for Kafka to become ready on port 9092..."
+ retry=0
+ until nc -z localhost 9092 2>/dev/null || (echo > /dev/tcp/localhost/9092) 2>/dev/null || [ $retry -eq 20 ]; do
+ sleep 2
+ retry=$((retry+1))
+ done
+
+ if [ $retry -lt 20 ]; then
+ echo "[INFO] Kafka container started successfully!"
+ else
+ echo "[WARNING] Kafka did not become ready on port 9092 within timeout."
+ fi
+ else
+ echo "[WARNING] Docker is not installed or not in PATH."
+ echo "[INFO] Please ensure Kafka is running locally on localhost:9092."
+ fi
+fi
+
+echo "[INFO] Compiling and running App.java..."
+../mvnw compile exec:java -Dexec.mainClass="com.iluwatar.messaging.App"
diff --git a/microservices-messaging/src/main/java/com/iluwatar/messaging/App.java b/microservices-messaging/src/main/java/com/iluwatar/messaging/App.java
new file mode 100644
index 000000000000..73c47c5ef1cf
--- /dev/null
+++ b/microservices-messaging/src/main/java/com/iluwatar/messaging/App.java
@@ -0,0 +1,161 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.messaging;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The Microservices Messaging pattern enables asynchronous communication between services through
+ * Apache Kafka. This example demonstrates how services can communicate without tight coupling.
+ *
+ * In this example:
+ *
+ *
+ * - OrderService acts as a message producer, publishing order events to Kafka
+ *
- InventoryService, PaymentService, and NotificationService act as consumers
+ *
- Apache Kafka acts as the message broker, routing messages between services
+ *
+ *
+ * Key benefits demonstrated:
+ *
+ *
+ * - Loose coupling - services don't directly depend on each other
+ *
- Asynchronous processing - producers don't wait for consumers
+ *
- Scalability - multiple consumers can process messages independently
+ *
- Resilience - if one consumer fails, others continue processing
+ *
- Message persistence - Kafka stores messages for reliability
+ *
+ *
+ * Prerequisites: This example requires a running Kafka instance. Start Kafka locally:
+ *
+ *
+ * # Start Zookeeper
+ * bin/zookeeper-server-start.sh config/zookeeper.properties
+ *
+ * # Start Kafka
+ * bin/kafka-server-start.sh config/server.properties
+ *
+ * # Create topic
+ * bin/kafka-topics.sh --create --topic order-topic --bootstrap-server localhost:9092
+ *
+ */
+public class App {
+ private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
+ private static final String BOOTSTRAP_SERVERS = "localhost:9092";
+
+ /** Sleep duration between operations in milliseconds. Package-private for testing. */
+ static long sleepMs = 2000;
+
+ /**
+ * Program entry point.
+ *
+ * @param args command line arguments
+ */
+ public static void main(String[] args) throws InterruptedException {
+ KafkaMessageProducer producer = new KafkaMessageProducer(BOOTSTRAP_SERVERS);
+
+ InventoryService inventoryService = new InventoryService();
+ PaymentService paymentService = new PaymentService();
+ NotificationService notificationService = new NotificationService();
+
+ KafkaMessageConsumer inventoryConsumer =
+ new KafkaMessageConsumer(
+ BOOTSTRAP_SERVERS, "inventory-group", "order-topic", inventoryService::handleMessage);
+
+ KafkaMessageConsumer paymentConsumer =
+ new KafkaMessageConsumer(
+ BOOTSTRAP_SERVERS, "payment-group", "order-topic", paymentService::handleMessage);
+
+ KafkaMessageConsumer notificationConsumer =
+ new KafkaMessageConsumer(
+ BOOTSTRAP_SERVERS,
+ "notification-group",
+ "order-topic",
+ notificationService::handleMessage);
+
+ run(producer, inventoryConsumer, paymentConsumer, notificationConsumer);
+ }
+
+ /**
+ * Runs the Microservices Messaging Pattern demonstration.
+ *
+ * @param producer the Kafka message producer
+ * @param inventoryConsumer the inventory service consumer
+ * @param paymentConsumer the payment service consumer
+ * @param notificationConsumer the notification service consumer
+ */
+ static void run(
+ KafkaMessageProducer producer,
+ KafkaMessageConsumer inventoryConsumer,
+ KafkaMessageConsumer paymentConsumer,
+ KafkaMessageConsumer notificationConsumer)
+ throws InterruptedException {
+ LOGGER.info("Starting Microservices Messaging Pattern with Apache Kafka");
+
+ // Start consumers in separate threads
+ ExecutorService executor = Executors.newFixedThreadPool(3);
+ executor.submit(inventoryConsumer);
+ executor.submit(paymentConsumer);
+ executor.submit(notificationConsumer);
+
+ // Give consumers time to subscribe
+ Thread.sleep(sleepMs);
+
+ // Create producer service
+ OrderService orderService = new OrderService(producer);
+
+ // Demonstrate the messaging pattern
+ LOGGER.info("\n=== Creating Order ===");
+ orderService.createOrder("ORDER-001");
+
+ Thread.sleep(sleepMs);
+
+ LOGGER.info("\n=== Updating Order ===");
+ orderService.updateOrder("ORDER-001");
+
+ Thread.sleep(sleepMs);
+
+ LOGGER.info("\n=== Cancelling Order ===");
+ orderService.cancelOrder("ORDER-001");
+
+ Thread.sleep(sleepMs);
+
+ // Cleanup
+ LOGGER.info("\nShutting down...");
+ inventoryConsumer.stop();
+ paymentConsumer.stop();
+ notificationConsumer.stop();
+ producer.close();
+
+ executor.shutdown();
+ executor.awaitTermination(5, TimeUnit.SECONDS);
+
+ LOGGER.info("Microservices Messaging Pattern demonstration completed");
+ }
+}
diff --git a/microservices-messaging/src/main/java/com/iluwatar/messaging/InventoryService.java b/microservices-messaging/src/main/java/com/iluwatar/messaging/InventoryService.java
new file mode 100644
index 000000000000..9552e867d1ee
--- /dev/null
+++ b/microservices-messaging/src/main/java/com/iluwatar/messaging/InventoryService.java
@@ -0,0 +1,86 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.messaging;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * InventoryService is a message consumer that processes inventory-related messages from Kafka. It
+ * listens to order events and updates inventory accordingly.
+ *
+ * This service runs in its own Kafka consumer group (inventory-group) which allows it to:
+ *
+ *
+ * - Process messages independently from other services
+ *
- Scale horizontally by adding more instances to the consumer group
+ *
- Resume from last committed offset if the service restarts
+ *
+ */
+public class InventoryService {
+ private static final Logger LOGGER = LoggerFactory.getLogger(InventoryService.class);
+
+ /**
+ * Handles incoming messages related to orders from Kafka.
+ *
+ * @param message the message to process
+ */
+ public void handleMessage(Message message) {
+ LOGGER.info(
+ "Inventory Service received message [{}]: {}", message.getId(), message.getContent());
+
+ if (message.getContent().contains("Order Created")) {
+ updateInventory(message);
+ } else if (message.getContent().contains("Order Cancelled")) {
+ restoreInventory(message);
+ } else {
+ LOGGER.debug("No inventory action needed for: {}", message.getContent());
+ }
+ }
+
+ private void updateInventory(Message message) {
+ LOGGER.info("Updating inventory for message: {}", message.getId());
+ // Simulate inventory update - reserve stock for the order
+ try {
+ Thread.sleep(100);
+ LOGGER.info("Inventory updated successfully for: {}", message.getContent());
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ LOGGER.error("Inventory update interrupted", e);
+ }
+ }
+
+ private void restoreInventory(Message message) {
+ LOGGER.info("Restoring inventory for message: {}", message.getId());
+ // Simulate inventory restoration - release reserved stock
+ try {
+ Thread.sleep(100);
+ LOGGER.info("Inventory restored successfully for: {}", message.getContent());
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ LOGGER.error("Inventory restore interrupted", e);
+ }
+ }
+}
diff --git a/microservices-messaging/src/main/java/com/iluwatar/messaging/KafkaMessageConsumer.java b/microservices-messaging/src/main/java/com/iluwatar/messaging/KafkaMessageConsumer.java
new file mode 100644
index 000000000000..71130e114dd8
--- /dev/null
+++ b/microservices-messaging/src/main/java/com/iluwatar/messaging/KafkaMessageConsumer.java
@@ -0,0 +1,125 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.messaging;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import java.time.Duration;
+import java.util.Collections;
+import java.util.Properties;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.apache.kafka.clients.consumer.Consumer;
+import org.apache.kafka.clients.consumer.ConsumerConfig;
+import org.apache.kafka.clients.consumer.ConsumerRecords;
+import org.apache.kafka.clients.consumer.KafkaConsumer;
+import org.apache.kafka.common.serialization.StringDeserializer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Kafka message consumer that subscribes to topics and processes messages. */
+public class KafkaMessageConsumer implements AutoCloseable, Runnable {
+ private static final Logger LOGGER = LoggerFactory.getLogger(KafkaMessageConsumer.class);
+ private final Consumer consumer;
+ private final ObjectMapper objectMapper;
+ private final String topic;
+ private final java.util.function.Consumer messageHandler;
+ private final AtomicBoolean running = new AtomicBoolean(true);
+
+ /**
+ * Creates a new Kafka message consumer.
+ *
+ * @param bootstrapServers Kafka bootstrap servers
+ * @param groupId consumer group ID
+ * @param topic topic to subscribe to
+ * @param messageHandler handler for received messages
+ */
+ public KafkaMessageConsumer(
+ String bootstrapServers,
+ String groupId,
+ String topic,
+ java.util.function.Consumer messageHandler) {
+ this(createDefaultConsumer(bootstrapServers, groupId), topic, messageHandler);
+ }
+
+ KafkaMessageConsumer(
+ Consumer consumer,
+ String topic,
+ java.util.function.Consumer messageHandler) {
+ this.consumer = consumer;
+ this.objectMapper = new ObjectMapper();
+ this.objectMapper.registerModule(new JavaTimeModule());
+ this.topic = topic;
+ this.messageHandler = messageHandler;
+ }
+
+ private static Consumer createDefaultConsumer(
+ String bootstrapServers, String groupId) {
+ Properties props = new Properties();
+ props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
+ props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
+ props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
+ props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
+ props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
+ props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
+ return new KafkaConsumer<>(props);
+ }
+
+ @Override
+ public void run() {
+ try {
+ consumer.subscribe(Collections.singletonList(topic));
+ LOGGER.info("Consumer subscribed to topic: {}", topic);
+
+ while (running.get()) {
+ ConsumerRecords records = consumer.poll(Duration.ofMillis(100));
+ records.forEach(
+ record -> {
+ try {
+ Message message = objectMapper.readValue(record.value(), Message.class);
+ LOGGER.info("Received message from topic '{}': {}", topic, message.getId());
+ messageHandler.accept(message);
+ } catch (Exception e) {
+ LOGGER.error("Error processing message: {}", e.getMessage(), e);
+ }
+ });
+ }
+ } catch (Exception e) {
+ LOGGER.error("Consumer error: {}", e.getMessage(), e);
+ } finally {
+ consumer.close();
+ LOGGER.info("Consumer closed for topic: {}", topic);
+ }
+ }
+
+ /** Stops the consumer. */
+ public void stop() {
+ running.set(false);
+ }
+
+ @Override
+ public void close() {
+ stop();
+ }
+}
diff --git a/microservices-messaging/src/main/java/com/iluwatar/messaging/KafkaMessageProducer.java b/microservices-messaging/src/main/java/com/iluwatar/messaging/KafkaMessageProducer.java
new file mode 100644
index 000000000000..56154152c008
--- /dev/null
+++ b/microservices-messaging/src/main/java/com/iluwatar/messaging/KafkaMessageProducer.java
@@ -0,0 +1,108 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.messaging;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import java.util.Properties;
+import org.apache.kafka.clients.producer.KafkaProducer;
+import org.apache.kafka.clients.producer.Producer;
+import org.apache.kafka.clients.producer.ProducerConfig;
+import org.apache.kafka.clients.producer.ProducerRecord;
+import org.apache.kafka.common.serialization.StringSerializer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Kafka message producer that publishes messages to Kafka topics. */
+public class KafkaMessageProducer implements AutoCloseable {
+ private static final Logger LOGGER = LoggerFactory.getLogger(KafkaMessageProducer.class);
+ private final Producer producer;
+ private final ObjectMapper objectMapper;
+
+ /**
+ * Creates a new Kafka message producer.
+ *
+ * @param bootstrapServers Kafka bootstrap servers
+ */
+ public KafkaMessageProducer(String bootstrapServers) {
+ this(createDefaultProducer(bootstrapServers));
+ }
+
+ KafkaMessageProducer(Producer producer) {
+ this.producer = producer;
+ this.objectMapper = new ObjectMapper();
+ this.objectMapper.registerModule(new JavaTimeModule());
+ }
+
+ private static Producer createDefaultProducer(String bootstrapServers) {
+ Properties props = new Properties();
+ props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
+ props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
+ props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
+ props.put(ProducerConfig.ACKS_CONFIG, "all");
+ props.put(ProducerConfig.RETRIES_CONFIG, 3);
+ return new KafkaProducer<>(props);
+ }
+
+ /**
+ * Publishes a message to a Kafka topic.
+ *
+ * @param topic the topic to publish to
+ * @param message the message to publish
+ */
+ public void publish(String topic, Message message) {
+ try {
+ String json = objectMapper.writeValueAsString(message);
+ ProducerRecord record = new ProducerRecord<>(topic, message.getId(), json);
+
+ producer.send(
+ record,
+ (metadata, exception) -> {
+ if (exception != null) {
+ LOGGER.error(
+ "Failed to publish message to topic {}: {}", topic, exception.getMessage());
+ } else {
+ LOGGER.info(
+ "Published message to topic '{}' [partition={}, offset={}]",
+ topic,
+ metadata.partition(),
+ metadata.offset());
+ }
+ });
+
+ } catch (Exception e) {
+ LOGGER.error("Error serializing message: {}", e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public void close() {
+ if (producer != null) {
+ producer.flush();
+ producer.close();
+ LOGGER.info("Kafka producer closed");
+ }
+ }
+}
diff --git a/microservices-messaging/src/main/java/com/iluwatar/messaging/Message.java b/microservices-messaging/src/main/java/com/iluwatar/messaging/Message.java
new file mode 100644
index 000000000000..5d0e9f96769c
--- /dev/null
+++ b/microservices-messaging/src/main/java/com/iluwatar/messaging/Message.java
@@ -0,0 +1,75 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.messaging;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.time.LocalDateTime;
+import java.util.UUID;
+import lombok.Getter;
+
+/** Represents a message exchanged between services. */
+@Getter
+public class Message {
+ private final String id;
+ private final String content;
+ private final LocalDateTime timestamp;
+
+ /**
+ * Creates a new message with the given content.
+ *
+ * @param content the message content
+ */
+ public Message(String content) {
+ this.id = UUID.randomUUID().toString();
+ this.content = content;
+ this.timestamp = LocalDateTime.now();
+ }
+
+ /** JSON constructor for deserialization. */
+ @JsonCreator
+ public Message(
+ @JsonProperty("id") String id,
+ @JsonProperty("content") String content,
+ @JsonProperty("timestamp") LocalDateTime timestamp) {
+ this.id = id;
+ this.content = content;
+ this.timestamp = timestamp;
+ }
+
+ @Override
+ public String toString() {
+ return "Message{"
+ + "id='"
+ + id
+ + '\''
+ + ", content='"
+ + content
+ + '\''
+ + ", timestamp="
+ + timestamp
+ + '}';
+ }
+}
diff --git a/microservices-messaging/src/main/java/com/iluwatar/messaging/NotificationService.java b/microservices-messaging/src/main/java/com/iluwatar/messaging/NotificationService.java
new file mode 100644
index 000000000000..461b1a326efb
--- /dev/null
+++ b/microservices-messaging/src/main/java/com/iluwatar/messaging/NotificationService.java
@@ -0,0 +1,100 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.messaging;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * NotificationService is a message consumer that processes notification-related messages from
+ * Kafka. It listens to order events and sends notifications to customers.
+ *
+ * This service runs in its own Kafka consumer group (notification-group) which allows it to:
+ *
+ *
+ * - Process messages independently from other services
+ *
- Scale horizontally by adding more instances to the consumer group
+ *
- Resume from last committed offset if the service restarts
+ *
+ */
+public class NotificationService {
+ private static final Logger LOGGER = LoggerFactory.getLogger(NotificationService.class);
+
+ /**
+ * Handles incoming messages related to orders from Kafka.
+ *
+ * @param message the message to process
+ */
+ public void handleMessage(Message message) {
+ LOGGER.info(
+ "Notification Service received message [{}]: {}", message.getId(), message.getContent());
+
+ if (message.getContent().contains("Order Created")) {
+ sendOrderConfirmation(message);
+ } else if (message.getContent().contains("Order Updated")) {
+ sendOrderUpdate(message);
+ } else if (message.getContent().contains("Order Cancelled")) {
+ sendCancellationNotice(message);
+ } else {
+ LOGGER.debug("No notification action needed for: {}", message.getContent());
+ }
+ }
+
+ private void sendOrderConfirmation(Message message) {
+ LOGGER.info("Sending order confirmation for message: {}", message.getId());
+ // Simulate sending email/SMS notification to customer
+ try {
+ Thread.sleep(50);
+ LOGGER.info("Order confirmation sent successfully for: {}", message.getContent());
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ LOGGER.error("Notification send interrupted", e);
+ }
+ }
+
+ private void sendOrderUpdate(Message message) {
+ LOGGER.info("Sending order update notification for message: {}", message.getId());
+ // Simulate sending update notification
+ try {
+ Thread.sleep(50);
+ LOGGER.info("Order update notification sent successfully for: {}", message.getContent());
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ LOGGER.error("Notification send interrupted", e);
+ }
+ }
+
+ private void sendCancellationNotice(Message message) {
+ LOGGER.info("Sending cancellation notice for message: {}", message.getId());
+ // Simulate sending cancellation notification
+ try {
+ Thread.sleep(50);
+ LOGGER.info("Cancellation notice sent successfully for: {}", message.getContent());
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ LOGGER.error("Notification send interrupted", e);
+ }
+ }
+}
diff --git a/microservices-messaging/src/main/java/com/iluwatar/messaging/OrderService.java b/microservices-messaging/src/main/java/com/iluwatar/messaging/OrderService.java
new file mode 100644
index 000000000000..d059e4e3e0fd
--- /dev/null
+++ b/microservices-messaging/src/main/java/com/iluwatar/messaging/OrderService.java
@@ -0,0 +1,76 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.messaging;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** OrderService is a message producer that publishes order-related messages using Kafka. */
+public class OrderService {
+ private static final Logger LOGGER = LoggerFactory.getLogger(OrderService.class);
+ private static final String ORDER_TOPIC = "order-topic";
+
+ private final KafkaMessageProducer producer;
+
+ public OrderService(KafkaMessageProducer producer) {
+ this.producer = producer;
+ }
+
+ /**
+ * Creates an order and publishes a message to notify other services.
+ *
+ * @param orderId the ID of the order to create
+ */
+ public void createOrder(String orderId) {
+ LOGGER.info("Creating order: {}", orderId);
+ Message message = new Message("Order Created: " + orderId);
+ producer.publish(ORDER_TOPIC, message);
+ LOGGER.info("Order creation message published for: {}", orderId);
+ }
+
+ /**
+ * Updates an order and publishes a message to notify other services.
+ *
+ * @param orderId the ID of the order to update
+ */
+ public void updateOrder(String orderId) {
+ LOGGER.info("Updating order: {}", orderId);
+ Message message = new Message("Order Updated: " + orderId);
+ producer.publish(ORDER_TOPIC, message);
+ LOGGER.info("Order update message published for: {}", orderId);
+ }
+
+ /**
+ * Cancels an order and publishes a message to notify other services.
+ *
+ * @param orderId the ID of the order to cancel
+ */
+ public void cancelOrder(String orderId) {
+ LOGGER.info("Cancelling order: {}", orderId);
+ Message message = new Message("Order Cancelled: " + orderId);
+ producer.publish(ORDER_TOPIC, message);
+ LOGGER.info("Order cancellation message published for: {}", orderId);
+ }
+}
diff --git a/microservices-messaging/src/main/java/com/iluwatar/messaging/PaymentService.java b/microservices-messaging/src/main/java/com/iluwatar/messaging/PaymentService.java
new file mode 100644
index 000000000000..4c90897a6e8f
--- /dev/null
+++ b/microservices-messaging/src/main/java/com/iluwatar/messaging/PaymentService.java
@@ -0,0 +1,85 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.messaging;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * PaymentService is a message consumer that processes payment-related messages from Kafka. It
+ * listens to order events and handles payment processing.
+ *
+ * This service runs in its own Kafka consumer group (payment-group) which allows it to:
+ *
+ *
+ * - Process messages independently from other services
+ *
- Scale horizontally by adding more instances to the consumer group
+ *
- Resume from last committed offset if the service restarts
+ *
+ */
+public class PaymentService {
+ private static final Logger LOGGER = LoggerFactory.getLogger(PaymentService.class);
+
+ /**
+ * Handles incoming messages related to orders from Kafka.
+ *
+ * @param message the message to process
+ */
+ public void handleMessage(Message message) {
+ LOGGER.info("Payment Service received message [{}]: {}", message.getId(), message.getContent());
+
+ if (message.getContent().contains("Order Created")) {
+ processPayment(message);
+ } else if (message.getContent().contains("Order Cancelled")) {
+ refundPayment(message);
+ } else {
+ LOGGER.debug("No payment action needed for: {}", message.getContent());
+ }
+ }
+
+ private void processPayment(Message message) {
+ LOGGER.info("Processing payment for message: {}", message.getId());
+ // Simulate payment processing - charge the customer
+ try {
+ Thread.sleep(150);
+ LOGGER.info("Payment processed successfully for: {}", message.getContent());
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ LOGGER.error("Payment processing interrupted", e);
+ }
+ }
+
+ private void refundPayment(Message message) {
+ LOGGER.info("Refunding payment for message: {}", message.getId());
+ // Simulate payment refund - return money to customer
+ try {
+ Thread.sleep(150);
+ LOGGER.info("Payment refunded successfully for: {}", message.getContent());
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ LOGGER.error("Payment refund interrupted", e);
+ }
+ }
+}
diff --git a/microservices-messaging/src/main/resources/logback.xml b/microservices-messaging/src/main/resources/logback.xml
new file mode 100644
index 000000000000..7f40741c0ff1
--- /dev/null
+++ b/microservices-messaging/src/main/resources/logback.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/microservices-messaging/src/test/java/com/iluwatar/messaging/AppTest.java b/microservices-messaging/src/test/java/com/iluwatar/messaging/AppTest.java
new file mode 100644
index 000000000000..430b44812e66
--- /dev/null
+++ b/microservices-messaging/src/test/java/com/iluwatar/messaging/AppTest.java
@@ -0,0 +1,85 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.apache.kafka.clients.consumer.MockConsumer;
+import org.apache.kafka.clients.consumer.OffsetResetStrategy;
+import org.apache.kafka.clients.producer.MockProducer;
+import org.apache.kafka.common.serialization.StringSerializer;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for {@link App}. Tests main application entry point. */
+class AppTest {
+
+ @BeforeEach
+ void setUp() {
+ // Speed up sleeps so tests finish instantly
+ App.sleepMs = 0;
+ }
+
+ @AfterEach
+ void tearDown() {
+ // Restore default so other contexts are unaffected
+ App.sleepMs = 2000;
+ }
+
+ @Test
+ void testAppConstructor() {
+ assertNotNull(new App(), "App should be instantiable");
+ }
+
+ @Test
+ void testRunWithMockObjects() {
+ // Build mock-backed producer and consumers — no Kafka broker required
+ MockProducer mockProducer =
+ new MockProducer<>(true, new StringSerializer(), new StringSerializer());
+ KafkaMessageProducer producer = new KafkaMessageProducer(mockProducer);
+
+ MockConsumer mc1 = new MockConsumer<>(OffsetResetStrategy.EARLIEST);
+ MockConsumer mc2 = new MockConsumer<>(OffsetResetStrategy.EARLIEST);
+ MockConsumer mc3 = new MockConsumer<>(OffsetResetStrategy.EARLIEST);
+
+ KafkaMessageConsumer inventoryConsumer =
+ new KafkaMessageConsumer(mc1, "order-topic", msg -> {});
+ KafkaMessageConsumer paymentConsumer = new KafkaMessageConsumer(mc2, "order-topic", msg -> {});
+ KafkaMessageConsumer notificationConsumer =
+ new KafkaMessageConsumer(mc3, "order-topic", msg -> {});
+
+ // Stop consumers immediately so their poll loops exit right away in the executor threads
+ inventoryConsumer.stop();
+ paymentConsumer.stop();
+ notificationConsumer.stop();
+
+ // sleepMs == 0, so all Thread.sleep(sleepMs) return instantly — full run() coverage
+ assertDoesNotThrow(
+ () -> App.run(producer, inventoryConsumer, paymentConsumer, notificationConsumer),
+ "App.run() should complete without throwing");
+ }
+}
diff --git a/microservices-messaging/src/test/java/com/iluwatar/messaging/InventoryServiceTest.java b/microservices-messaging/src/test/java/com/iluwatar/messaging/InventoryServiceTest.java
new file mode 100644
index 000000000000..bb0842ce7aef
--- /dev/null
+++ b/microservices-messaging/src/test/java/com/iluwatar/messaging/InventoryServiceTest.java
@@ -0,0 +1,118 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link InventoryService}. Tests service behavior with various message types
+ * without Kafka dependencies.
+ */
+class InventoryServiceTest {
+
+ private InventoryService inventoryService;
+
+ @BeforeEach
+ void setUp() {
+ inventoryService = new InventoryService();
+ }
+
+ @Test
+ void testServiceCanBeInstantiated() {
+ // Arrange & Act & Assert
+ assertNotNull(inventoryService, "InventoryService should be instantiated");
+ }
+
+ @Test
+ void testHandleOrderCreatedMessage() {
+ // Arrange
+ var message = new Message("Order Created: ORDER-001");
+
+ // Act & Assert
+ assertDoesNotThrow(
+ () -> inventoryService.handleMessage(message),
+ "Should handle order created message without error");
+ }
+
+ @Test
+ void testHandleOrderCancelledMessage() {
+ // Arrange
+ var message = new Message("Order Cancelled: ORDER-001");
+
+ // Act & Assert
+ assertDoesNotThrow(
+ () -> inventoryService.handleMessage(message),
+ "Should handle order cancelled message without error");
+ }
+
+ @Test
+ void testHandleOrderUpdatedMessage() {
+ // Arrange
+ var message = new Message("Order Updated: ORDER-001");
+
+ // Act & Assert
+ assertDoesNotThrow(
+ () -> inventoryService.handleMessage(message),
+ "Should handle order updated message without error");
+ }
+
+ @Test
+ void testHandleUnknownMessage() {
+ // Arrange
+ var message = new Message("Unknown Event: ORDER-001");
+
+ // Act & Assert
+ assertDoesNotThrow(
+ () -> inventoryService.handleMessage(message),
+ "Should handle unknown message without error");
+ }
+
+ @Test
+ void testHandleMultipleMessages() {
+ // Act & Assert
+ assertDoesNotThrow(
+ () -> {
+ inventoryService.handleMessage(new Message("Order Created: ORDER-001"));
+ inventoryService.handleMessage(new Message("Order Updated: ORDER-001"));
+ inventoryService.handleMessage(new Message("Order Cancelled: ORDER-001"));
+ },
+ "Should handle multiple messages without error");
+ }
+
+ @Test
+ void testHandleMessagesWhenInterrupted() {
+ Thread.currentThread().interrupt();
+ inventoryService.handleMessage(new Message("Order Created: ORDER-001"));
+ org.junit.jupiter.api.Assertions.assertTrue(Thread.interrupted());
+
+ Thread.currentThread().interrupt();
+ inventoryService.handleMessage(new Message("Order Cancelled: ORDER-001"));
+ org.junit.jupiter.api.Assertions.assertTrue(Thread.interrupted());
+ }
+}
diff --git a/microservices-messaging/src/test/java/com/iluwatar/messaging/KafkaMessageConsumerTest.java b/microservices-messaging/src/test/java/com/iluwatar/messaging/KafkaMessageConsumerTest.java
new file mode 100644
index 000000000000..2977f7448c56
--- /dev/null
+++ b/microservices-messaging/src/test/java/com/iluwatar/messaging/KafkaMessageConsumerTest.java
@@ -0,0 +1,137 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.apache.kafka.clients.consumer.MockConsumer;
+import org.apache.kafka.clients.consumer.OffsetResetStrategy;
+import org.apache.kafka.common.TopicPartition;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for {@link KafkaMessageConsumer}. */
+class KafkaMessageConsumerTest {
+
+ private MockConsumer mockConsumer;
+ private KafkaMessageConsumer kafkaMessageConsumer;
+ private AtomicBoolean handlerCalled;
+
+ @BeforeEach
+ void setUp() {
+ mockConsumer = new MockConsumer<>(OffsetResetStrategy.EARLIEST);
+ handlerCalled = new AtomicBoolean(false);
+ kafkaMessageConsumer =
+ new KafkaMessageConsumer(mockConsumer, "test-topic", msg -> handlerCalled.set(true));
+ }
+
+ @Test
+ void testConsumerCanBeInstantiated() {
+ assertNotNull(kafkaMessageConsumer, "KafkaMessageConsumer should be instantiated");
+ }
+
+ @Test
+ void testRunProcessesValidMessageAndStops() throws Exception {
+ ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule());
+ Message msg = new Message("Order Created: 123");
+ String jsonStr = mapper.writeValueAsString(msg);
+
+ TopicPartition tp = new TopicPartition("test-topic", 0);
+ mockConsumer.updateBeginningOffsets(
+ new HashMap<>() {
+ {
+ put(tp, 0L);
+ }
+ });
+
+ mockConsumer.schedulePollTask(
+ () -> {
+ mockConsumer.rebalance(Collections.singletonList(tp));
+ mockConsumer.addRecord(new ConsumerRecord<>("test-topic", 0, 0L, "key", jsonStr));
+ });
+
+ mockConsumer.schedulePollTask(() -> kafkaMessageConsumer.stop());
+
+ kafkaMessageConsumer.run();
+
+ assertTrue(handlerCalled.get(), "Handler should have been invoked");
+ assertTrue(mockConsumer.closed(), "Consumer should be closed");
+ }
+
+ @Test
+ void testRunHandlesInvalidJsonMessage() {
+ TopicPartition tp = new TopicPartition("test-topic", 0);
+ mockConsumer.updateBeginningOffsets(
+ new HashMap<>() {
+ {
+ put(tp, 0L);
+ }
+ });
+
+ mockConsumer.schedulePollTask(
+ () -> {
+ mockConsumer.rebalance(Collections.singletonList(tp));
+ mockConsumer.addRecord(new ConsumerRecord<>("test-topic", 0, 0L, "key", "{invalid json"));
+ });
+
+ mockConsumer.schedulePollTask(() -> kafkaMessageConsumer.stop());
+
+ assertDoesNotThrow(() -> kafkaMessageConsumer.run());
+ assertTrue(mockConsumer.closed(), "Consumer should be closed");
+ }
+
+ @Test
+ void testCloseStopsConsumer() {
+ assertDoesNotThrow(() -> kafkaMessageConsumer.close());
+ }
+
+ @Test
+ void testRunWhenAlreadyStopped() {
+ // Stop the consumer before run() so the while loop exits immediately
+ kafkaMessageConsumer.stop();
+
+ // run() should complete without processing any records
+ assertDoesNotThrow(() -> kafkaMessageConsumer.run());
+ assertTrue(mockConsumer.closed(), "Consumer should be closed even when stopped before run");
+ }
+
+ @Test
+ void testRunHandlesConsumerException() {
+ // Schedule a WakeupException during poll to trigger the outer catch block
+ mockConsumer.schedulePollTask(() -> mockConsumer.wakeup());
+
+ // run() must not propagate the exception
+ assertDoesNotThrow(() -> kafkaMessageConsumer.run());
+ assertTrue(mockConsumer.closed(), "Consumer should be closed after exception");
+ }
+}
diff --git a/microservices-messaging/src/test/java/com/iluwatar/messaging/KafkaMessageProducerTest.java b/microservices-messaging/src/test/java/com/iluwatar/messaging/KafkaMessageProducerTest.java
new file mode 100644
index 000000000000..1257495813a3
--- /dev/null
+++ b/microservices-messaging/src/test/java/com/iluwatar/messaging/KafkaMessageProducerTest.java
@@ -0,0 +1,93 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.kafka.clients.producer.MockProducer;
+import org.apache.kafka.common.serialization.StringSerializer;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for {@link KafkaMessageProducer}. */
+class KafkaMessageProducerTest {
+
+ private MockProducer mockProducer;
+ private KafkaMessageProducer kafkaMessageProducer;
+
+ @BeforeEach
+ void setUp() {
+ mockProducer = new MockProducer<>(true, new StringSerializer(), new StringSerializer());
+ kafkaMessageProducer = new KafkaMessageProducer(mockProducer);
+ }
+
+ @Test
+ void testProducerCanBeInstantiated() {
+ assertNotNull(kafkaMessageProducer, "KafkaMessageProducer should be instantiated");
+ }
+
+ @Test
+ void testPublishMessageSuccess() {
+ Message message = new Message("Test Order");
+
+ assertDoesNotThrow(() -> kafkaMessageProducer.publish("test-topic", message));
+ assertEquals(1, mockProducer.history().size());
+ assertEquals("test-topic", mockProducer.history().get(0).topic());
+ assertEquals(message.getId(), mockProducer.history().get(0).key());
+ }
+
+ @Test
+ void testPublishMessageErrorCallback() {
+ MockProducer failingProducer =
+ new MockProducer<>(false, new StringSerializer(), new StringSerializer());
+ KafkaMessageProducer producerWithError = new KafkaMessageProducer(failingProducer);
+ Message message = new Message("Test Order");
+
+ assertDoesNotThrow(() -> producerWithError.publish("test-topic", message));
+ assertEquals(1, failingProducer.history().size());
+
+ // Trigger error callback BEFORE closing so the exception != null branch is covered
+ failingProducer.errorNext(new RuntimeException("Kafka publish error"));
+
+ assertDoesNotThrow(() -> producerWithError.close());
+ assertTrue(failingProducer.closed());
+ }
+
+ @Test
+ void testClose() {
+ assertDoesNotThrow(() -> kafkaMessageProducer.close());
+ assertTrue(mockProducer.closed());
+ }
+
+ @Test
+ void testPublishNullMessageCatchesException() {
+ // Passing null causes message.getId() to throw NPE, which is caught by the
+ // catch(Exception e) block — covering the "Error serializing message" log branch.
+ assertDoesNotThrow(() -> kafkaMessageProducer.publish("test-topic", null));
+ }
+}
diff --git a/microservices-messaging/src/test/java/com/iluwatar/messaging/MessageTest.java b/microservices-messaging/src/test/java/com/iluwatar/messaging/MessageTest.java
new file mode 100644
index 000000000000..d80cc2b806ca
--- /dev/null
+++ b/microservices-messaging/src/test/java/com/iluwatar/messaging/MessageTest.java
@@ -0,0 +1,165 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import java.time.LocalDateTime;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link Message}. Tests follow FIRST principles: Fast, Isolated, Repeatable,
+ * Self-validating, Timely.
+ */
+class MessageTest {
+
+ private ObjectMapper objectMapper;
+
+ @BeforeEach
+ void setUp() {
+ objectMapper = new ObjectMapper();
+ objectMapper.registerModule(new JavaTimeModule());
+ }
+
+ @Test
+ void testMessageCreation() {
+ // Arrange & Act
+ var message = new Message("Test content");
+
+ // Assert
+ assertNotNull(message.getId(), "Message ID should not be null");
+ assertEquals("Test content", message.getContent(), "Content should match");
+ assertNotNull(message.getTimestamp(), "Timestamp should not be null");
+ }
+
+ @Test
+ void testMessageIdIsUnique() {
+ // Arrange & Act
+ var message1 = new Message("Content 1");
+ var message2 = new Message("Content 2");
+
+ // Assert
+ assertNotEquals(message1.getId(), message2.getId(), "Each message should have unique ID");
+ }
+
+ @Test
+ void testMessageTimestamp() {
+ // Arrange
+ var beforeCreation = LocalDateTime.now();
+
+ // Act
+ var message = new Message("Test");
+ var afterCreation = LocalDateTime.now();
+
+ // Assert
+ assertTrue(
+ message.getTimestamp().isAfter(beforeCreation.minusSeconds(1))
+ && message.getTimestamp().isBefore(afterCreation.plusSeconds(1)),
+ "Timestamp should be close to creation time");
+ }
+
+ @Test
+ void testJsonSerialization() throws Exception {
+ // Arrange
+ var originalMessage = new Message("Test content");
+
+ // Act
+ var json = objectMapper.writeValueAsString(originalMessage);
+
+ // Assert
+ assertNotNull(json, "JSON should not be null");
+ assertTrue(json.contains("Test content"), "JSON should contain content");
+ assertTrue(json.contains(originalMessage.getId()), "JSON should contain ID");
+ }
+
+ @Test
+ void testJsonDeserialization() throws Exception {
+ // Arrange
+ var originalMessage = new Message("Test content");
+ var json = objectMapper.writeValueAsString(originalMessage);
+
+ // Act
+ var deserializedMessage = objectMapper.readValue(json, Message.class);
+
+ // Assert
+ assertNotNull(deserializedMessage, "Deserialized message should not be null");
+ assertEquals(originalMessage.getId(), deserializedMessage.getId(), "IDs should match");
+ assertEquals(
+ originalMessage.getContent(), deserializedMessage.getContent(), "Content should match");
+ }
+
+ @Test
+ void testToString() {
+ // Arrange
+ var message = new Message("Test content");
+
+ // Act
+ var result = message.toString();
+
+ // Assert
+ assertNotNull(result, "ToString should not return null");
+ assertTrue(result.contains("Message{"), "ToString should contain class name");
+ assertTrue(result.contains("Test content"), "ToString should contain content");
+ assertTrue(result.contains(message.getId()), "ToString should contain ID");
+ }
+
+ @Test
+ void testMessageWithEmptyContent() {
+ // Arrange & Act
+ var message = new Message("");
+
+ // Assert
+ assertNotNull(message.getId(), "ID should be generated even for empty content");
+ assertEquals("", message.getContent(), "Empty content should be preserved");
+ }
+
+ @Test
+ void testMessageWithNullContent() {
+ // Arrange & Act
+ var message = new Message(null);
+
+ // Assert
+ assertNotNull(message.getId(), "ID should be generated even for null content");
+ assertEquals(null, message.getContent(), "Null content should be preserved");
+ }
+
+ @Test
+ void testMessageWithSpecialCharacters() {
+ // Arrange
+ var specialContent = "Test with special chars: @#$%^&*()";
+
+ // Act
+ var message = new Message(specialContent);
+
+ // Assert
+ assertEquals(specialContent, message.getContent(), "Special characters should be preserved");
+ }
+}
diff --git a/microservices-messaging/src/test/java/com/iluwatar/messaging/NotificationServiceTest.java b/microservices-messaging/src/test/java/com/iluwatar/messaging/NotificationServiceTest.java
new file mode 100644
index 000000000000..142e59087b5c
--- /dev/null
+++ b/microservices-messaging/src/test/java/com/iluwatar/messaging/NotificationServiceTest.java
@@ -0,0 +1,134 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link NotificationService}. Tests service behavior with various message types
+ * without Kafka dependencies.
+ */
+class NotificationServiceTest {
+
+ private NotificationService notificationService;
+
+ @BeforeEach
+ void setUp() {
+ notificationService = new NotificationService();
+ }
+
+ @Test
+ void testServiceCanBeInstantiated() {
+ // Arrange & Act & Assert
+ assertNotNull(notificationService, "NotificationService should be instantiated");
+ }
+
+ @Test
+ void testHandleOrderCreatedMessage() {
+ // Arrange
+ var message = new Message("Order Created: ORDER-001");
+
+ // Act & Assert
+ assertDoesNotThrow(
+ () -> notificationService.handleMessage(message),
+ "Should handle order created message without error");
+ }
+
+ @Test
+ void testHandleOrderUpdatedMessage() {
+ // Arrange
+ var message = new Message("Order Updated: ORDER-001");
+
+ // Act & Assert
+ assertDoesNotThrow(
+ () -> notificationService.handleMessage(message),
+ "Should handle order updated message without error");
+ }
+
+ @Test
+ void testHandleOrderCancelledMessage() {
+ // Arrange
+ var message = new Message("Order Cancelled: ORDER-001");
+
+ // Act & Assert
+ assertDoesNotThrow(
+ () -> notificationService.handleMessage(message),
+ "Should handle order cancelled message without error");
+ }
+
+ @Test
+ void testHandleUnknownMessage() {
+ // Arrange
+ var message = new Message("Unknown Event: ORDER-001");
+
+ // Act & Assert
+ assertDoesNotThrow(
+ () -> notificationService.handleMessage(message),
+ "Should handle unknown message without error");
+ }
+
+ @Test
+ void testHandleAllMessageTypes() {
+ // Act & Assert
+ assertDoesNotThrow(
+ () -> {
+ notificationService.handleMessage(new Message("Order Created: ORDER-001"));
+ notificationService.handleMessage(new Message("Order Updated: ORDER-001"));
+ notificationService.handleMessage(new Message("Order Cancelled: ORDER-001"));
+ },
+ "Should handle all message types without error");
+ }
+
+ @Test
+ void testHandleMultipleOrdersSequentially() {
+ // Act & Assert
+ assertDoesNotThrow(
+ () -> {
+ notificationService.handleMessage(new Message("Order Created: ORDER-001"));
+ notificationService.handleMessage(new Message("Order Created: ORDER-002"));
+ notificationService.handleMessage(new Message("Order Created: ORDER-003"));
+ },
+ "Should handle multiple orders sequentially without error");
+ }
+
+ @Test
+ void testHandleMessagesWhenInterrupted() {
+ Thread.currentThread().interrupt();
+ notificationService.handleMessage(new Message("Order Created: ORDER-001"));
+ org.junit.jupiter.api.Assertions.assertTrue(Thread.interrupted());
+
+ Thread.currentThread().interrupt();
+ notificationService.handleMessage(new Message("Order Updated: ORDER-001"));
+ org.junit.jupiter.api.Assertions.assertTrue(Thread.interrupted());
+
+ Thread.currentThread().interrupt();
+ notificationService.handleMessage(new Message("Order Cancelled: ORDER-001"));
+ org.junit.jupiter.api.Assertions.assertTrue(Thread.interrupted());
+ }
+}
diff --git a/microservices-messaging/src/test/java/com/iluwatar/messaging/OrderServiceTest.java b/microservices-messaging/src/test/java/com/iluwatar/messaging/OrderServiceTest.java
new file mode 100644
index 000000000000..2b3f38584a20
--- /dev/null
+++ b/microservices-messaging/src/test/java/com/iluwatar/messaging/OrderServiceTest.java
@@ -0,0 +1,109 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.apache.kafka.clients.producer.MockProducer;
+import org.apache.kafka.common.serialization.StringSerializer;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for {@link OrderService}. Tests follow Arrange-Act-Assert pattern. */
+class OrderServiceTest {
+
+ private MockProducer mockKafkaProducer;
+ private KafkaMessageProducer messageProducer;
+ private OrderService orderService;
+
+ @BeforeEach
+ void setUp() {
+ mockKafkaProducer = new MockProducer<>(true, new StringSerializer(), new StringSerializer());
+ messageProducer = new KafkaMessageProducer(mockKafkaProducer);
+ orderService = new OrderService(messageProducer);
+ }
+
+ @Test
+ void testCreateOrder() {
+ // Arrange
+ var orderId = "ORDER-001";
+
+ // Act
+ assertDoesNotThrow(() -> orderService.createOrder(orderId));
+
+ // Assert
+ assertEquals(1, mockKafkaProducer.history().size());
+ }
+
+ @Test
+ void testUpdateOrder() {
+ // Arrange
+ var orderId = "ORDER-002";
+
+ // Act
+ assertDoesNotThrow(() -> orderService.updateOrder(orderId));
+
+ // Assert
+ assertEquals(1, mockKafkaProducer.history().size());
+ }
+
+ @Test
+ void testCancelOrder() {
+ // Arrange
+ var orderId = "ORDER-003";
+
+ // Act
+ assertDoesNotThrow(() -> orderService.cancelOrder(orderId));
+
+ // Assert
+ assertEquals(1, mockKafkaProducer.history().size());
+ }
+
+ @Test
+ void testMultipleOrderOperations() {
+ // Arrange
+ var orderId = "ORDER-004";
+
+ // Act
+ orderService.createOrder(orderId);
+ orderService.updateOrder(orderId);
+ orderService.cancelOrder(orderId);
+
+ // Assert
+ assertEquals(3, mockKafkaProducer.history().size());
+ }
+
+ @Test
+ void testCreateOrderWithDifferentIds() {
+ // Act
+ orderService.createOrder("ORDER-001");
+ orderService.createOrder("ORDER-002");
+ orderService.createOrder("ORDER-003");
+
+ // Assert
+ assertEquals(3, mockKafkaProducer.history().size());
+ }
+}
diff --git a/microservices-messaging/src/test/java/com/iluwatar/messaging/PaymentServiceTest.java b/microservices-messaging/src/test/java/com/iluwatar/messaging/PaymentServiceTest.java
new file mode 100644
index 000000000000..608a64549c87
--- /dev/null
+++ b/microservices-messaging/src/test/java/com/iluwatar/messaging/PaymentServiceTest.java
@@ -0,0 +1,116 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.messaging;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link PaymentService}. Tests service behavior with various message types without
+ * Kafka dependencies.
+ */
+class PaymentServiceTest {
+
+ private PaymentService paymentService;
+
+ @BeforeEach
+ void setUp() {
+ paymentService = new PaymentService();
+ }
+
+ @Test
+ void testServiceCanBeInstantiated() {
+ // Arrange & Act & Assert
+ assertNotNull(paymentService, "PaymentService should be instantiated");
+ }
+
+ @Test
+ void testHandleOrderCreatedMessage() {
+ // Arrange
+ var message = new Message("Order Created: ORDER-001");
+
+ // Act & Assert
+ assertDoesNotThrow(
+ () -> paymentService.handleMessage(message),
+ "Should handle order created message without error");
+ }
+
+ @Test
+ void testHandleOrderCancelledMessage() {
+ // Arrange
+ var message = new Message("Order Cancelled: ORDER-001");
+
+ // Act & Assert
+ assertDoesNotThrow(
+ () -> paymentService.handleMessage(message),
+ "Should handle order cancelled message without error");
+ }
+
+ @Test
+ void testHandleOrderUpdatedMessage() {
+ // Arrange
+ var message = new Message("Order Updated: ORDER-001");
+
+ // Act & Assert
+ assertDoesNotThrow(
+ () -> paymentService.handleMessage(message),
+ "Should handle order updated message without error");
+ }
+
+ @Test
+ void testHandleUnknownMessage() {
+ // Arrange
+ var message = new Message("Unknown Event: ORDER-001");
+
+ // Act & Assert
+ assertDoesNotThrow(
+ () -> paymentService.handleMessage(message), "Should handle unknown message without error");
+ }
+
+ @Test
+ void testHandleMultipleMessages() {
+ // Act & Assert
+ assertDoesNotThrow(
+ () -> {
+ paymentService.handleMessage(new Message("Order Created: ORDER-001"));
+ paymentService.handleMessage(new Message("Order Cancelled: ORDER-001"));
+ },
+ "Should handle multiple messages without error");
+ }
+
+ @Test
+ void testHandleMessagesWhenInterrupted() {
+ Thread.currentThread().interrupt();
+ paymentService.handleMessage(new Message("Order Created: ORDER-001"));
+ org.junit.jupiter.api.Assertions.assertTrue(Thread.interrupted());
+
+ Thread.currentThread().interrupt();
+ paymentService.handleMessage(new Message("Order Cancelled: ORDER-001"));
+ org.junit.jupiter.api.Assertions.assertTrue(Thread.interrupted());
+ }
+}
diff --git a/microservices-self-registration/contextservice/pom.xml b/microservices-self-registration/contextservice/pom.xml
index 71f61fcf6a30..964ebdecee4e 100644
--- a/microservices-self-registration/contextservice/pom.xml
+++ b/microservices-self-registration/contextservice/pom.xml
@@ -70,7 +70,7 @@
com.diffplug.spotless
spotless-maven-plugin
- 2.44.4
+ 3.8.0
diff --git a/microservices-self-registration/eurekaserver/pom.xml b/microservices-self-registration/eurekaserver/pom.xml
index fac49a1e4938..d8b923b813cf 100644
--- a/microservices-self-registration/eurekaserver/pom.xml
+++ b/microservices-self-registration/eurekaserver/pom.xml
@@ -52,7 +52,7 @@
com.diffplug.spotless
spotless-maven-plugin
- 2.44.4
+ 3.8.0
diff --git a/microservices-self-registration/greetingservice/pom.xml b/microservices-self-registration/greetingservice/pom.xml
index 36221db34add..b6a92aabc8a3 100644
--- a/microservices-self-registration/greetingservice/pom.xml
+++ b/microservices-self-registration/greetingservice/pom.xml
@@ -66,7 +66,7 @@
com.diffplug.spotless
spotless-maven-plugin
- 2.44.4
+ 3.8.0
diff --git a/onion-architecture/README.md b/onion-architecture/README.md
new file mode 100644
index 000000000000..1a7fc394638d
--- /dev/null
+++ b/onion-architecture/README.md
@@ -0,0 +1,292 @@
+---
+title: "Onion Architecture in Java: A Layered Approach to Building Maintainable and Testable Applications"
+shortTitle: Onion Architecture
+description: "Learn how the Onion Architecture pattern promotes maintainability, testability, and separation of concerns in Java applications. Explore examples, benefits, and best practices."
+category: Architectural
+language: en
+tag:
+ - Decoupling
+ - Enterprise patterns
+ - Integration
+ - Microservices
+ - Scalability
+ - Security
+---
+## Intent of Microservices API Gateway Design Pattern
+
+In this project, the implementation demonstrates **Onion Architecture** with clear dependency direction: infrastructure and application depend on domain, but domain is independent.
+
+The central intent is to keep business rules in `domain`, orchestrate use cases in `application`, and isolate delivery/persistence/API details in `infrastructure`.
+
+Current implementation highlights:
+
+* `domain`: `Person`, `Category`, `PersonRepository`, `DomainException`
+* `application`: `SavePersonUseCase`, `GetPersonUseCase`, DTO records
+* `infrastructure`: Spring Boot REST controller, JPA entities, repository adapter, bean wiring
+
+## Also known as
+
+* Ports and Adapters Architecture
+* Hexagonal-style layering (conceptually related)
+* Dependency-rule-first architecture
+
+## Detailed Explanation of Onion Architecture Pattern with Real-World Examples
+
+Real-world example
+
+> Imagine a people-management service where business validation must stay consistent no matter how data is stored or exposed. In this codebase, `Person` and `Category` enforce invariants (e.g., age >= 18, required email/category), use cases coordinate behavior, and infrastructure adapts HTTP + JPA concerns. This allows the persistence or web layer to evolve without changing core domain rules.
+
+In plain words
+
+> The project keeps business logic in the center and treats frameworks as replaceable details around it.
+
+Wikipedia says
+
+> Onion Architecture is a software architecture pattern that emphasizes separation of concerns and dependency inversion by organizing code in concentric layers, with the domain model at the center.
+
+Sequence diagram
+
+
+
+Request flow in this implementation:
+
+1. Client calls REST endpoint in `PersonController` (`/api/persons`, `/api/persons/{id}`)
+2. Controller delegates to `SavePersonUseCase` or `GetPersonUseCase`
+3. Use case interacts with `PersonRepository` abstraction from `domain`
+4. `PersonRepositoryAdapter` maps domain <-> JPA and delegates to `SpringDataPersonRepository`
+5. Response DTO (`PersonResponse`) is returned to the client
+
+## Programmatic Example of Onion Architecture in Java
+
+This repository exposes a simple person API backed by use cases and domain models.
+
+Controller (infrastructure layer):
+
+```java
+@RestController
+@RequestMapping("/api")
+public class PersonController {
+
+ @GetMapping("/persons/{id}")
+ public ResponseEntity getPerson(@PathVariable Long id) {
+ var person = getPersonUseCase.execute(id);
+ return ResponseEntity.ok(person);
+ }
+
+ @GetMapping("/persons")
+ public ResponseEntity> getAllPersons() {
+ var persons = getPersonUseCase.executeAll();
+ return ResponseEntity.ok(persons);
+ }
+
+ @PostMapping("/persons")
+ public ResponseEntity savePerson(@RequestBody SavePersonCommand command) {
+ try {
+ var savedPerson = savePersonUseCase.execute(command);
+ return ResponseEntity.status(HttpStatus.OK).body(savedPerson);
+ } catch (DomainException e) {
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
+ }
+ }
+}
+```
+
+Use case (application layer):
+
+```java
+public class SavePersonUseCase {
+
+ private final PersonRepository repository;
+
+ public PersonResponse execute(SavePersonCommand command) {
+ var category = new Category(command.categoryId(), command.categoryType());
+ var person = new Person(
+ null,
+ command.firstName(),
+ command.lastName(),
+ command.age(),
+ command.phoneNumber(),
+ command.email(),
+ category);
+
+ var savedPerson = repository.save(person);
+ return new PersonResponse(
+ savedPerson.getId(),
+ savedPerson.getFirstName(),
+ savedPerson.getLastName(),
+ savedPerson.getAge(),
+ savedPerson.getPhoneNumber(),
+ savedPerson.getEmail(),
+ savedPerson.getCategory().getId(),
+ savedPerson.getCategory().getType()
+ );
+ }
+}
+```
+
+Domain validation (domain layer):
+
+```java
+public class Person {
+ public Person(Long id, String firstName, String lastName, int age,
+ String phoneNumber, String email, Category category) {
+ validateNames(firstName, lastName);
+ validateAge(age);
+ validatePhone(phoneNumber);
+ validateEmail(email);
+ validateCategory(category);
+ // assign fields...
+ }
+}
+```
+
+Repository adapter (infrastructure -> domain port):
+
+```java
+@Repository
+public class PersonRepositoryAdapter implements PersonRepository {
+
+ private final SpringDataPersonRepository repository;
+
+ @Override
+ public Optional findById(Long id) {
+ return repository.findById(id).map(this::mapToDomain);
+ }
+
+ @Override
+ public Person save(Person person) {
+ JpaPersonEntity savedEntity = repository.save(mapToEntity(person));
+ return mapToDomain(savedEntity);
+ }
+}
+```
+
+- **Maven 3.6.0** or higher
+
+### Build Steps
+
+1. **Navigate to the onion-architecture module directory:**
+ ```bash
+ cd java-design-patterns/onion-architecture
+ ```
+
+2. **Build all modules:**
+ ```bash
+ mvn clean package
+ ```
+ This will compile the `domain`, `application`, and `infrastructure` modules and package them into a Spring Boot executable JAR.
+
+3. **Run the Spring Boot application:**
+ ```bash
+ mvn -pl infrastructure spring-boot:run
+ ```
+ Alternatively, after building, run the JAR directly:
+ ```bash
+ java -jar infrastructure/target/infrastructure-1.26.0-SNAPSHOT.jar
+ ```
+
+### Accessing the API
+
+The application exposes REST endpoints at `http://localhost:8080/api`:
+There is a Postman collection available in the `etc/postman` folder for testing the API.
+
+- **Get all persons:**
+ ```bash
+ GET http://localhost:8080/api/persons
+ ```
+
+- **Get person by ID:**
+ ```bash
+ GET http://localhost:8080/api/persons/{id}
+ ```
+
+- **Create a new person:**
+ ```bash
+ POST http://localhost:8080/api/persons
+ Content-Type: application/json
+
+ {
+ "firstName": "John",
+ "lastName": "Doe",
+ "age": 30,
+ "phoneNumber": "555-1234",
+ "email": "john.doe@example.com",
+ "address": "123 Main St",
+ "categoryId": 1,
+ "categoryType": "individual"
+ }
+ ```
+
+### Run Tests
+
+To execute unit tests across all modules:
+
+```bash
+mvn clean test
+```
+
+To run tests for a specific module:
+
+```bash
+mvn -pl domain test
+mvn -pl application test
+mvn -pl infrastructure test
+```
+
+### Database
+
+The application uses an **H2 in-memory database** for demonstration purposes. Configuration is in `infrastructure/src/main/resources/application.properties`:
+
+- **JDBC URL:** `jdbc:h2:mem:testdb`
+- **Username:** `sa`
+- **Password:** `password`
+
+Sample data is initialized from `infrastructure/src/main/resources/data.sql` on application startup.
+
+## When to Use the Onion Architecture Pattern in Java
+
+* When domain rules must be stable and independent from frameworks.
+* When you want use cases to be testable without HTTP or database setup.
+* When infrastructure details (web, JPA, database) should be replaceable.
+* When dependency direction must be enforced from outer layers toward the domain core.
+
+## Onion Architecture Pattern Java Tutorials
+
+* [Clean Architecture with Spring Boot (Baeldung)](https://www.baeldung.com/spring-boot-clean-architecture)
+* [Hexagonal Architecture Explained (Cockburn)](https://alistair.cockburn.us/hexagonal-architecture)
+* [Spring Data JPA Reference](https://docs.spring.io/spring-data/jpa/reference/)
+
+## Benefits and Trade-offs of Microservices API Gateway Pattern
+
+Benefits:
+
+* Business validations are centralized in domain constructors (`Person`, `Category`).
+* Use cases stay independent from Spring, JPA, and transport concerns.
+* Repository abstraction (`PersonRepository`) keeps application logic persistence-agnostic.
+* Testability is strong across layers (domain, use case, adapter, controller tests).
+
+Trade-offs:
+
+* Additional mapping code between domain models, DTOs, and JPA entities.
+* More classes and modules than a simple CRUD-by-controller approach.
+* Requires discipline to avoid leaking infrastructure concerns into domain/application.
+
+## Real-World Applications of Microservices API Gateway Pattern in Java
+
+* People/contact management services with strict data validation.
+* Internal platforms where multiple delivery mechanisms (REST, batch, messaging) can share the same core domain.
+* Systems that need incremental infrastructure evolution while preserving business logic.
+
+## Related Java Design Patterns
+
+* [Repository](https://martinfowler.com/eaaCatalog/repository.html) - `PersonRepository` defines the domain-facing persistence contract.
+* [Adapter](https://refactoring.guru/design-patterns/adapter) - `PersonRepositoryAdapter` bridges domain model and Spring Data JPA.
+* [Dependency Injection](https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html) - `ApplicationConfig` wires use case beans with repository implementations.
+
+## References and Credits
+
+* Project modules: `domain`, `application`, `infrastructure`
+* Java 21 + Maven multi-module setup
+* Spring Boot 3.3 (`spring-boot-starter-web`, `spring-boot-starter-data-jpa`, H2)
+* Layer-focused tests in each module validating domain invariants and use case behavior
+
diff --git a/onion-architecture/application/pom.xml b/onion-architecture/application/pom.xml
new file mode 100644
index 000000000000..eccdaae2869a
--- /dev/null
+++ b/onion-architecture/application/pom.xml
@@ -0,0 +1,66 @@
+
+
+
+ 4.0.0
+
+ com.iluwatar
+ onion-architecture
+ 1.26.0-SNAPSHOT
+
+ application
+ Application
+
+
+
+ com.iluwatar
+ domain
+ ${project.version}
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+ org.mockito
+ mockito-junit-jupiter
+ test
+
+
+
diff --git a/onion-architecture/application/src/main/java/com/iluwatar/onion/application/dto/PersonResponse.java b/onion-architecture/application/src/main/java/com/iluwatar/onion/application/dto/PersonResponse.java
new file mode 100644
index 000000000000..1586f30e48a1
--- /dev/null
+++ b/onion-architecture/application/src/main/java/com/iluwatar/onion/application/dto/PersonResponse.java
@@ -0,0 +1,37 @@
+/*
+ *
+ * * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ * *
+ * * The MIT License
+ * * Copyright © 2014-2022 Ilkka Seppälä
+ * *
+ * * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * * of this software and associated documentation files (the "Software"), to deal
+ * * in the Software without restriction, including without limitation the rights
+ * * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * * copies of the Software, and to permit persons to whom the Software is
+ * * furnished to do so, subject to the following conditions:
+ * *
+ * * The above copyright notice and this permission notice shall be included in
+ * * all copies or substantial portions of the Software.
+ * *
+ * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * * THE SOFTWARE.
+ *
+ */
+package com.iluwatar.onion.application.dto;
+
+public record PersonResponse(
+ Long id,
+ String firstName,
+ String lastName,
+ int age,
+ String phoneNumber,
+ String email,
+ Long categoryId,
+ String categoryType) {}
diff --git a/onion-architecture/application/src/main/java/com/iluwatar/onion/application/dto/SavePersonCommand.java b/onion-architecture/application/src/main/java/com/iluwatar/onion/application/dto/SavePersonCommand.java
new file mode 100644
index 000000000000..649db88a4ab5
--- /dev/null
+++ b/onion-architecture/application/src/main/java/com/iluwatar/onion/application/dto/SavePersonCommand.java
@@ -0,0 +1,37 @@
+/*
+ *
+ * * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ * *
+ * * The MIT License
+ * * Copyright © 2014-2022 Ilkka Seppälä
+ * *
+ * * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * * of this software and associated documentation files (the "Software"), to deal
+ * * in the Software without restriction, including without limitation the rights
+ * * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * * copies of the Software, and to permit persons to whom the Software is
+ * * furnished to do so, subject to the following conditions:
+ * *
+ * * The above copyright notice and this permission notice shall be included in
+ * * all copies or substantial portions of the Software.
+ * *
+ * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * * THE SOFTWARE.
+ *
+ */
+package com.iluwatar.onion.application.dto;
+
+public record SavePersonCommand(
+ String firstName,
+ String lastName,
+ int age,
+ String phoneNumber,
+ String email,
+ String address,
+ Long categoryId,
+ String categoryType) {}
diff --git a/onion-architecture/application/src/main/java/com/iluwatar/onion/application/usecase/GetPersonUseCase.java b/onion-architecture/application/src/main/java/com/iluwatar/onion/application/usecase/GetPersonUseCase.java
new file mode 100644
index 000000000000..abae6a32196a
--- /dev/null
+++ b/onion-architecture/application/src/main/java/com/iluwatar/onion/application/usecase/GetPersonUseCase.java
@@ -0,0 +1,75 @@
+/*
+ *
+ * * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ * *
+ * * The MIT License
+ * * Copyright © 2014-2022 Ilkka Seppälä
+ * *
+ * * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * * of this software and associated documentation files (the "Software"), to deal
+ * * in the Software without restriction, including without limitation the rights
+ * * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * * copies of the Software, and to permit persons to whom the Software is
+ * * furnished to do so, subject to the following conditions:
+ * *
+ * * The above copyright notice and this permission notice shall be included in
+ * * all copies or substantial portions of the Software.
+ * *
+ * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * * THE SOFTWARE.
+ *
+ */
+package com.iluwatar.onion.application.usecase;
+
+import com.iluwatar.onion.application.dto.PersonResponse;
+import com.iluwatar.onion.domain.repository.PersonRepository;
+import java.util.Collection;
+import java.util.List;
+
+public class GetPersonUseCase {
+
+ private final PersonRepository repository;
+
+ public GetPersonUseCase(PersonRepository repository) {
+ this.repository = repository;
+ }
+
+ public PersonResponse execute(Long id) {
+ var person =
+ repository
+ .findById(id)
+ .orElseThrow(() -> new RuntimeException("Person not found with id: " + id));
+
+ return new PersonResponse(
+ person.getId(),
+ person.getFirstName(),
+ person.getLastName(),
+ person.getAge(),
+ person.getPhoneNumber(),
+ person.getEmail(),
+ person.getCategory().getId(),
+ person.getCategory().getType());
+ }
+
+ public List executeAll() {
+ return repository.findAll().stream()
+ .flatMap(Collection::stream)
+ .map(
+ person ->
+ new PersonResponse(
+ person.getId(),
+ person.getFirstName(),
+ person.getLastName(),
+ person.getAge(),
+ person.getPhoneNumber(),
+ person.getEmail(),
+ person.getCategory().getId(),
+ person.getCategory().getType()))
+ .toList();
+ }
+}
diff --git a/onion-architecture/application/src/main/java/com/iluwatar/onion/application/usecase/SavePersonUseCase.java b/onion-architecture/application/src/main/java/com/iluwatar/onion/application/usecase/SavePersonUseCase.java
new file mode 100644
index 000000000000..8d63e2612fab
--- /dev/null
+++ b/onion-architecture/application/src/main/java/com/iluwatar/onion/application/usecase/SavePersonUseCase.java
@@ -0,0 +1,67 @@
+/*
+ *
+ * * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ * *
+ * * The MIT License
+ * * Copyright © 2014-2022 Ilkka Seppälä
+ * *
+ * * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * * of this software and associated documentation files (the "Software"), to deal
+ * * in the Software without restriction, including without limitation the rights
+ * * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * * copies of the Software, and to permit persons to whom the Software is
+ * * furnished to do so, subject to the following conditions:
+ * *
+ * * The above copyright notice and this permission notice shall be included in
+ * * all copies or substantial portions of the Software.
+ * *
+ * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * * THE SOFTWARE.
+ *
+ */
+package com.iluwatar.onion.application.usecase;
+
+import com.iluwatar.onion.application.dto.PersonResponse;
+import com.iluwatar.onion.application.dto.SavePersonCommand;
+import com.iluwatar.onion.domain.model.Category;
+import com.iluwatar.onion.domain.model.Person;
+import com.iluwatar.onion.domain.repository.PersonRepository;
+
+public class SavePersonUseCase {
+
+ private final PersonRepository repository;
+
+ public SavePersonUseCase(PersonRepository repository) {
+ this.repository = repository;
+ }
+
+ public PersonResponse execute(SavePersonCommand command) {
+ var category = new Category(command.categoryId(), command.categoryType());
+ var person =
+ new Person(
+ null,
+ command.firstName(),
+ command.lastName(),
+ command.age(),
+ command.phoneNumber(),
+ command.email(),
+ category);
+
+ var savedPerson = repository.save(person);
+
+ return new PersonResponse(
+ savedPerson.getId(),
+ savedPerson.getFirstName(),
+ savedPerson.getLastName(),
+ savedPerson.getAge(),
+ savedPerson.getPhoneNumber(),
+ savedPerson.getEmail(),
+ savedPerson.getCategory().getId(),
+ savedPerson.getCategory().getType());
+ }
+}
diff --git a/onion-architecture/application/src/test/java/com/iluwatar/onion/application/usecase/GetPersonUseCaseTest.java b/onion-architecture/application/src/test/java/com/iluwatar/onion/application/usecase/GetPersonUseCaseTest.java
new file mode 100644
index 000000000000..f9de119c2309
--- /dev/null
+++ b/onion-architecture/application/src/test/java/com/iluwatar/onion/application/usecase/GetPersonUseCaseTest.java
@@ -0,0 +1,193 @@
+/*
+ *
+ * * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ * *
+ * * The MIT License
+ * * Copyright © 2014-2022 Ilkka Seppälä
+ * *
+ * * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * * of this software and associated documentation files (the "Software"), to deal
+ * * in the Software without restriction, including without limitation the rights
+ * * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * * copies of the Software, and to permit persons to whom the Software is
+ * * furnished to do so, subject to the following conditions:
+ * *
+ * * The above copyright notice and this permission notice shall be included in
+ * * all copies or substantial portions of the Software.
+ * *
+ * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * * THE SOFTWARE.
+ *
+ */
+package com.iluwatar.onion.application.usecase;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import com.iluwatar.onion.application.dto.PersonResponse;
+import com.iluwatar.onion.domain.model.Category;
+import com.iluwatar.onion.domain.model.Person;
+import com.iluwatar.onion.domain.repository.PersonRepository;
+import java.util.List;
+import java.util.Optional;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class GetPersonUseCaseTest {
+
+ @Mock private PersonRepository personRepository;
+
+ private GetPersonUseCase getPersonUseCase;
+
+ @BeforeEach
+ void setUp() {
+ getPersonUseCase = new GetPersonUseCase(personRepository);
+ }
+
+ @Nested
+ @DisplayName("Execute by ID - Happy Path")
+ class ExecuteByIdHappyPath {
+
+ @Test
+ @DisplayName("Should return PersonResponse when person exists")
+ void shouldReturnPersonResponseWhenPersonExists() {
+ // Arrange
+ var person =
+ new Person(
+ 1L,
+ "John",
+ "Doe",
+ 25,
+ "+1234567890",
+ "john.doe@example.com",
+ new Category(1L, "Professional"));
+
+ when(personRepository.findById(1L)).thenReturn(Optional.of(person));
+
+ // Act
+ var response = getPersonUseCase.execute(1L);
+
+ // Assert
+ assertNotNull(response);
+ assertEquals(1L, response.id());
+ assertEquals("John", response.firstName());
+ assertEquals("Doe", response.lastName());
+ assertEquals(25, response.age());
+ assertEquals("+1234567890", response.phoneNumber());
+ assertEquals("john.doe@example.com", response.email());
+ assertEquals(1L, response.categoryId());
+ assertEquals("Professional", response.categoryType());
+
+ verify(personRepository, times(1)).findById(1L);
+ }
+ }
+
+ @Nested
+ @DisplayName("Execute by ID - Error Cases")
+ class ExecuteByIdErrorCases {
+
+ @Test
+ @DisplayName("Should throw RuntimeException when person not found")
+ void shouldThrowExceptionWhenPersonNotFound() {
+ // Arrange
+ when(personRepository.findById(999L)).thenReturn(Optional.empty());
+
+ // Act & Assert
+ var exception = assertThrows(RuntimeException.class, () -> getPersonUseCase.execute(999L));
+ assertTrue(exception.getMessage().contains("Person not found with id: 999"));
+
+ verify(personRepository, times(1)).findById(999L);
+ }
+
+ @Test
+ @DisplayName("Should propagate exception when repository throws exception")
+ void shouldPropagateExceptionWhenRepositoryFails() {
+ // Arrange
+ when(personRepository.findById(anyLong())).thenThrow(new RuntimeException("Database error"));
+
+ // Act & Assert
+ var exception = assertThrows(RuntimeException.class, () -> getPersonUseCase.execute(1L));
+ assertTrue(exception.getMessage().contains("Database error"));
+
+ verify(personRepository, times(1)).findById(1L);
+ }
+ }
+
+ @Nested
+ @DisplayName("Execute All - Happy Path")
+ class ExecuteAllHappyPath {
+
+ @Test
+ @DisplayName("Should return list of PersonResponses")
+ void shouldReturnListOfPersonResponses() {
+ // Arrange
+ var person1 =
+ new Person(
+ 1L,
+ "John",
+ "Doe",
+ 30,
+ "+9876543255",
+ "john.doe@example.com",
+ new Category(2L, "Personal"));
+
+ var person2 =
+ new Person(
+ 2L,
+ "Jane",
+ "Smith",
+ 25,
+ "+9876543210",
+ "jane.smith@example.com",
+ new Category(2L, "Personal"));
+
+ when(personRepository.findAll()).thenReturn(Optional.of(List.of(person1, person2)));
+
+ // Act
+ var responses = getPersonUseCase.executeAll();
+
+ // Assert
+ assertNotNull(responses);
+ assertEquals(2, responses.size());
+
+ PersonResponse response1 = responses.get(0);
+ assertEquals(1L, response1.id());
+ assertEquals("John", response1.firstName());
+ assertEquals("Doe", response1.lastName());
+
+ PersonResponse response2 = responses.get(1);
+ assertEquals(2L, response2.id());
+ assertEquals("Jane", response2.firstName());
+ assertEquals("Smith", response2.lastName());
+
+ verify(personRepository, times(1)).findAll();
+ }
+
+ @Test
+ @DisplayName("Should return empty list when no persons found")
+ void shouldReturnEmptyListWhenNoPersonsFound() {
+ // Arrange
+ when(personRepository.findAll()).thenReturn(Optional.of(List.of()));
+
+ // Act
+ List responses = getPersonUseCase.executeAll();
+
+ // Assert
+ assertNotNull(responses);
+ assertTrue(responses.isEmpty());
+
+ verify(personRepository, times(1)).findAll();
+ }
+ }
+}
diff --git a/onion-architecture/application/src/test/java/com/iluwatar/onion/application/usecase/SavePersonUseCaseTest.java b/onion-architecture/application/src/test/java/com/iluwatar/onion/application/usecase/SavePersonUseCaseTest.java
new file mode 100644
index 000000000000..434147ae6495
--- /dev/null
+++ b/onion-architecture/application/src/test/java/com/iluwatar/onion/application/usecase/SavePersonUseCaseTest.java
@@ -0,0 +1,253 @@
+/*
+ *
+ * * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ * *
+ * * The MIT License
+ * * Copyright © 2014-2022 Ilkka Seppälä
+ * *
+ * * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * * of this software and associated documentation files (the "Software"), to deal
+ * * in the Software without restriction, including without limitation the rights
+ * * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * * copies of the Software, and to permit persons to whom the Software is
+ * * furnished to do so, subject to the following conditions:
+ * *
+ * * The above copyright notice and this permission notice shall be included in
+ * * all copies or substantial portions of the Software.
+ * *
+ * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * * THE SOFTWARE.
+ *
+ */
+package com.iluwatar.onion.application.usecase;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+import com.iluwatar.onion.application.dto.SavePersonCommand;
+import com.iluwatar.onion.domain.exception.DomainException;
+import com.iluwatar.onion.domain.model.Category;
+import com.iluwatar.onion.domain.model.Person;
+import com.iluwatar.onion.domain.repository.PersonRepository;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class SavePersonUseCaseTest {
+
+ @Mock private PersonRepository personRepository;
+
+ @Captor private ArgumentCaptor personCaptor;
+
+ private SavePersonUseCase savePersonUseCase;
+
+ @BeforeEach
+ void setUp() {
+ savePersonUseCase = new SavePersonUseCase(personRepository);
+ }
+
+ @Nested
+ @DisplayName("Execute - Happy Path")
+ class ExecuteHappyPath {
+
+ @Test
+ @DisplayName("Should save person and return PersonResponse")
+ void shouldSavePersonAndReturnResponse() {
+ // Arrange
+ var command =
+ new SavePersonCommand(
+ "John",
+ "Doe",
+ 25,
+ "+1234567890",
+ "john.doe@example.com",
+ "123 Main St",
+ 1L,
+ "Professional");
+
+ var category = new Category(1L, "Professional");
+ var savedPerson =
+ new Person(
+ 100L, // ID generated by database
+ "John",
+ "Doe",
+ 25,
+ "+1234567890",
+ "john.doe@example.com",
+ category);
+
+ when(personRepository.save(any(Person.class))).thenReturn(savedPerson);
+
+ // Act
+ var response = savePersonUseCase.execute(command);
+
+ // Assert
+ assertNotNull(response);
+ assertEquals(100L, response.id());
+ assertEquals("John", response.firstName());
+ assertEquals("Doe", response.lastName());
+ assertEquals(25, response.age());
+ assertEquals("+1234567890", response.phoneNumber());
+ assertEquals("john.doe@example.com", response.email());
+ assertEquals(1L, response.categoryId());
+ assertEquals("Professional", response.categoryType());
+
+ // Verify repository interaction
+ verify(personRepository, times(1)).save(any(Person.class));
+ }
+
+ @Test
+ @DisplayName("Should pass correct Person object to repository")
+ void shouldPassCorrectPersonToRepository() {
+ // Arrange
+ var command =
+ new SavePersonCommand(
+ "Jane",
+ "Smith",
+ 30,
+ "+9876543210",
+ "jane.smith@example.com",
+ "456 Oak Ave",
+ 2L,
+ "Personal");
+
+ var category = new Category(2L, "Personal");
+ var savedPerson =
+ new Person(200L, "Jane", "Smith", 30, "+9876543210", "jane.smith@example.com", category);
+
+ when(personRepository.save(any(Person.class))).thenReturn(savedPerson);
+
+ // Act
+ savePersonUseCase.execute(command);
+
+ // Assert - Verify what was passed to repository
+ verify(personRepository).save(personCaptor.capture());
+ var capturedPerson = personCaptor.getValue();
+
+ assertNull(capturedPerson.getId()); // New person should have null ID
+ assertEquals("Jane", capturedPerson.getFirstName());
+ assertEquals("Smith", capturedPerson.getLastName());
+ assertEquals(30, capturedPerson.getAge());
+ assertEquals("+9876543210", capturedPerson.getPhoneNumber());
+ assertEquals("jane.smith@example.com", capturedPerson.getEmail());
+ assertEquals("Personal", capturedPerson.getCategory().getType());
+ }
+ }
+
+ @Nested
+ @DisplayName("Execute - Validation Failures")
+ class ExecuteValidationFailures {
+
+ @Test
+ @DisplayName("Should throw DomainException when age is less than 18")
+ void shouldThrowExceptionWhenAgeIsInvalid() {
+ // Arrange
+ var command =
+ new SavePersonCommand(
+ "Young",
+ "Person",
+ 17, // Invalid age
+ "+1234567890",
+ "young@example.com",
+ "123 Main St",
+ 1L,
+ "Student");
+
+ // Act & Assert
+ var exception = assertThrows(DomainException.class, () -> savePersonUseCase.execute(command));
+ assertTrue(exception.getMessage().contains("Age cannot be less than 18"));
+
+ // Verify repository was never called
+ verify(personRepository, never()).save(any(Person.class));
+ }
+
+ @Test
+ @DisplayName("Should throw DomainException when email is empty")
+ void shouldThrowExceptionWhenEmailIsEmpty() {
+ // Arrange
+ var command =
+ new SavePersonCommand(
+ "John",
+ "Doe",
+ 25,
+ "+1234567890",
+ "", // Empty email
+ "123 Main St",
+ 1L,
+ "Professional");
+
+ // Act & Assert
+ var exception = assertThrows(DomainException.class, () -> savePersonUseCase.execute(command));
+ assertTrue(exception.getMessage().contains("Email cannot be null or empty"));
+
+ verify(personRepository, never()).save(any(Person.class));
+ }
+
+ @Test
+ @DisplayName("Should throw DomainException when category type is invalid")
+ void shouldThrowExceptionWhenCategoryTypeIsInvalid() {
+ // Arrange
+ var command =
+ new SavePersonCommand(
+ "John",
+ "Doe",
+ 25,
+ "+1234567890",
+ "john@example.com",
+ "123 Main St",
+ 1L,
+ "" // Empty category type
+ );
+
+ // Act & Assert
+ var exception = assertThrows(DomainException.class, () -> savePersonUseCase.execute(command));
+ assertTrue(exception.getMessage().contains("Type is null or empty"));
+
+ verify(personRepository, never()).save(any(Person.class));
+ }
+ }
+
+ @Nested
+ @DisplayName("Execute - Repository Exceptions")
+ class ExecuteRepositoryExceptions {
+
+ @Test
+ @DisplayName("Should propagate exception when repository fails")
+ void shouldPropagateExceptionWhenRepositoryFails() {
+ // Arrange
+ var command =
+ new SavePersonCommand(
+ "John",
+ "Doe",
+ 25,
+ "+1234567890",
+ "john.doe@example.com",
+ "123 Main St",
+ 1L,
+ "Professional");
+
+ when(personRepository.save(any(Person.class)))
+ .thenThrow(new RuntimeException("Database connection failed"));
+
+ // Act & Assert
+ var exception =
+ assertThrows(RuntimeException.class, () -> savePersonUseCase.execute(command));
+ assertTrue(exception.getMessage().contains("Database connection failed"));
+
+ verify(personRepository, times(1)).save(any(Person.class));
+ }
+ }
+}
diff --git a/onion-architecture/domain/pom.xml b/onion-architecture/domain/pom.xml
new file mode 100644
index 000000000000..d1efc50def12
--- /dev/null
+++ b/onion-architecture/domain/pom.xml
@@ -0,0 +1,47 @@
+
+
+
+ 4.0.0
+
+ com.iluwatar
+ onion-architecture
+ 1.26.0-SNAPSHOT
+
+ domain
+ Domain
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+
diff --git a/onion-architecture/domain/src/main/java/com/iluwatar/onion/domain/exception/DomainException.java b/onion-architecture/domain/src/main/java/com/iluwatar/onion/domain/exception/DomainException.java
new file mode 100644
index 000000000000..5e0831a57c02
--- /dev/null
+++ b/onion-architecture/domain/src/main/java/com/iluwatar/onion/domain/exception/DomainException.java
@@ -0,0 +1,34 @@
+/*
+ *
+ * * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ * *
+ * * The MIT License
+ * * Copyright © 2014-2022 Ilkka Seppälä
+ * *
+ * * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * * of this software and associated documentation files (the "Software"), to deal
+ * * in the Software without restriction, including without limitation the rights
+ * * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * * copies of the Software, and to permit persons to whom the Software is
+ * * furnished to do so, subject to the following conditions:
+ * *
+ * * The above copyright notice and this permission notice shall be included in
+ * * all copies or substantial portions of the Software.
+ * *
+ * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * * THE SOFTWARE.
+ *
+ */
+package com.iluwatar.onion.domain.exception;
+
+public class DomainException extends RuntimeException {
+
+ public DomainException(String message) {
+ super(message);
+ }
+}
diff --git a/onion-architecture/domain/src/main/java/com/iluwatar/onion/domain/model/Category.java b/onion-architecture/domain/src/main/java/com/iluwatar/onion/domain/model/Category.java
new file mode 100644
index 000000000000..8380f4e849ee
--- /dev/null
+++ b/onion-architecture/domain/src/main/java/com/iluwatar/onion/domain/model/Category.java
@@ -0,0 +1,51 @@
+/*
+ *
+ * * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ * *
+ * * The MIT License
+ * * Copyright © 2014-2022 Ilkka Seppälä
+ * *
+ * * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * * of this software and associated documentation files (the "Software"), to deal
+ * * in the Software without restriction, including without limitation the rights
+ * * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * * copies of the Software, and to permit persons to whom the Software is
+ * * furnished to do so, subject to the following conditions:
+ * *
+ * * The above copyright notice and this permission notice shall be included in
+ * * all copies or substantial portions of the Software.
+ * *
+ * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * * THE SOFTWARE.
+ *
+ */
+package com.iluwatar.onion.domain.model;
+
+import com.iluwatar.onion.domain.exception.DomainException;
+
+public class Category {
+
+ private final Long id;
+ private final String type;
+
+ public Category(Long id, String type) {
+ if (type == null || type.isEmpty()) {
+ throw new DomainException("Type is null or empty. Category type is required.");
+ }
+ this.id = id;
+ this.type = type;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public String getType() {
+ return type;
+ }
+}
diff --git a/onion-architecture/domain/src/main/java/com/iluwatar/onion/domain/model/Person.java b/onion-architecture/domain/src/main/java/com/iluwatar/onion/domain/model/Person.java
new file mode 100644
index 000000000000..55eced0be29a
--- /dev/null
+++ b/onion-architecture/domain/src/main/java/com/iluwatar/onion/domain/model/Person.java
@@ -0,0 +1,121 @@
+/*
+ *
+ * * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ * *
+ * * The MIT License
+ * * Copyright © 2014-2022 Ilkka Seppälä
+ * *
+ * * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * * of this software and associated documentation files (the "Software"), to deal
+ * * in the Software without restriction, including without limitation the rights
+ * * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * * copies of the Software, and to permit persons to whom the Software is
+ * * furnished to do so, subject to the following conditions:
+ * *
+ * * The above copyright notice and this permission notice shall be included in
+ * * all copies or substantial portions of the Software.
+ * *
+ * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * * THE SOFTWARE.
+ *
+ */
+package com.iluwatar.onion.domain.model;
+
+import com.iluwatar.onion.domain.exception.DomainException;
+
+public class Person {
+
+ private final Long id;
+ private final String firstName;
+ private final String lastName;
+ private final int age;
+ private final String phoneNumber;
+ private final String email;
+ private final Category category;
+
+ public Person(
+ Long id,
+ String firstName,
+ String lastName,
+ int age,
+ String phoneNumber,
+ String email,
+ Category category) {
+ validateNames(firstName, lastName);
+ validateAge(age);
+ validatePhone(phoneNumber);
+ validateEmail(email);
+ validateCategory(category);
+
+ this.id = id;
+ this.firstName = firstName;
+ this.lastName = lastName;
+ this.age = age;
+ this.phoneNumber = phoneNumber;
+ this.email = email;
+ this.category = category;
+ }
+
+ private void validateNames(String firstName, String lastName) {
+ if (firstName == null || lastName == null) {
+ throw new DomainException("First name and last name cannot be null.");
+ }
+ }
+
+ private void validateAge(int age) {
+ if (age < 18) {
+ throw new DomainException("Age cannot be less than 18.");
+ }
+ }
+
+ private void validatePhone(String phone) {
+ if (phone == null || phone.isEmpty()) {
+ throw new DomainException("Phone number cannot be null or empty.");
+ }
+ }
+
+ private void validateEmail(String email) {
+ if (email == null || email.isEmpty()) {
+ throw new DomainException("Email cannot be null or empty.");
+ }
+ }
+
+ private void validateCategory(Category category) {
+ if (category == null || category.getType().isEmpty()) {
+ throw new DomainException("Category cannot be null or empty.");
+ }
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public int getAge() {
+ return age;
+ }
+
+ public String getPhoneNumber() {
+ return phoneNumber;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public Category getCategory() {
+ return category;
+ }
+}
diff --git a/onion-architecture/domain/src/main/java/com/iluwatar/onion/domain/repository/PersonRepository.java b/onion-architecture/domain/src/main/java/com/iluwatar/onion/domain/repository/PersonRepository.java
new file mode 100644
index 000000000000..c14aa24a8ef6
--- /dev/null
+++ b/onion-architecture/domain/src/main/java/com/iluwatar/onion/domain/repository/PersonRepository.java
@@ -0,0 +1,45 @@
+/*
+ *
+ * * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ * *
+ * * The MIT License
+ * * Copyright © 2014-2022 Ilkka Seppälä
+ * *
+ * * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * * of this software and associated documentation files (the "Software"), to deal
+ * * in the Software without restriction, including without limitation the rights
+ * * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * * copies of the Software, and to permit persons to whom the Software is
+ * * furnished to do so, subject to the following conditions:
+ * *
+ * * The above copyright notice and this permission notice shall be included in
+ * * all copies or substantial portions of the Software.
+ * *
+ * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * * THE SOFTWARE.
+ *
+ */
+package com.iluwatar.onion.domain.repository;
+
+import com.iluwatar.onion.domain.model.Person;
+import java.util.List;
+import java.util.Optional;
+
+public interface PersonRepository {
+ Optional findById(Long id);
+
+ Optional findByFirstName(String firstName);
+
+ Optional findByLastName(String lastName);
+
+ Optional> findAll();
+
+ Person save(Person person);
+
+ boolean deleteById(Long id);
+}
diff --git a/onion-architecture/domain/src/test/java/com/iluwatar/onion/domain/model/CategoryTest.java b/onion-architecture/domain/src/test/java/com/iluwatar/onion/domain/model/CategoryTest.java
new file mode 100644
index 000000000000..82e88285c35b
--- /dev/null
+++ b/onion-architecture/domain/src/test/java/com/iluwatar/onion/domain/model/CategoryTest.java
@@ -0,0 +1,75 @@
+/*
+ *
+ * * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ * *
+ * * The MIT License
+ * * Copyright © 2014-2022 Ilkka Seppälä
+ * *
+ * * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * * of this software and associated documentation files (the "Software"), to deal
+ * * in the Software without restriction, including without limitation the rights
+ * * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * * copies of the Software, and to permit persons to whom the Software is
+ * * furnished to do so, subject to the following conditions:
+ * *
+ * * The above copyright notice and this permission notice shall be included in
+ * * all copies or substantial portions of the Software.
+ * *
+ * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * * THE SOFTWARE.
+ *
+ */
+package com.iluwatar.onion.domain.model;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import com.iluwatar.onion.domain.exception.DomainException;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class CategoryTest {
+
+ @Test
+ @DisplayName("Should create category with valid data")
+ void shouldCreateCategoryWithValidData() {
+ // Arrange & Act
+ var category = new Category(1L, "Professional");
+
+ // Assert
+ assertNotNull(category);
+ assertEquals(1L, category.getId());
+ assertEquals("Professional", category.getType());
+ }
+
+ @Test
+ @DisplayName("Should create category with null id")
+ void shouldCreateCategoryWithNullId() {
+ // Arrange & Act
+ var category = new Category(null, "Personal");
+
+ // Assert
+ assertNull(category.getId());
+ assertEquals("Personal", category.getType());
+ }
+
+ @Test
+ @DisplayName("Should throw exception when type is null")
+ void shouldThrowExceptionWhenTypeIsNull() {
+ // Act & Assert
+ var exception = assertThrows(DomainException.class, () -> new Category(1L, null));
+ assertTrue(exception.getMessage().contains("Type is null or empty"));
+ }
+
+ @Test
+ @DisplayName("Should throw exception when type is empty")
+ void shouldThrowExceptionWhenTypeIsEmpty() {
+ // Act & Assert
+ var exception = assertThrows(DomainException.class, () -> new Category(1L, ""));
+ assertTrue(exception.getMessage().contains("Type is null or empty"));
+ }
+}
diff --git a/onion-architecture/domain/src/test/java/com/iluwatar/onion/domain/model/PersonTest.java b/onion-architecture/domain/src/test/java/com/iluwatar/onion/domain/model/PersonTest.java
new file mode 100644
index 000000000000..8f5bef0da1a6
--- /dev/null
+++ b/onion-architecture/domain/src/test/java/com/iluwatar/onion/domain/model/PersonTest.java
@@ -0,0 +1,179 @@
+/*
+ *
+ * * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ * *
+ * * The MIT License
+ * * Copyright © 2014-2022 Ilkka Seppälä
+ * *
+ * * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * * of this software and associated documentation files (the "Software"), to deal
+ * * in the Software without restriction, including without limitation the rights
+ * * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * * copies of the Software, and to permit persons to whom the Software is
+ * * furnished to do so, subject to the following conditions:
+ * *
+ * * The above copyright notice and this permission notice shall be included in
+ * * all copies or substantial portions of the Software.
+ * *
+ * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * * THE SOFTWARE.
+ *
+ */
+package com.iluwatar.onion.domain.model;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import com.iluwatar.onion.domain.exception.DomainException;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+class PersonTest {
+
+ private final Category validCategory = new Category(1L, "Professional");
+
+ @Nested
+ @DisplayName("Person Creation - Valid Cases")
+ class ValidPersonCreation {
+
+ @Test
+ @DisplayName("Should create person with valid data")
+ void shouldCreatePersonWithValidData() {
+ // Arrange & Act
+ var person =
+ new Person(1L, "John", "Doe", 25, "+1234567890", "john.doe@example.com", validCategory);
+
+ // Assert
+ assertNotNull(person);
+ assertEquals(1L, person.getId());
+ assertEquals("John", person.getFirstName());
+ assertEquals("Doe", person.getLastName());
+ assertEquals(25, person.getAge());
+ assertEquals("+1234567890", person.getPhoneNumber());
+ assertEquals("john.doe@example.com", person.getEmail());
+ assertEquals(validCategory, person.getCategory());
+ }
+
+ @Test
+ @DisplayName("Should create person with minimum valid age (18)")
+ void shouldCreatePersonWithMinimumValidAge() {
+ // Arrange & Act
+ var person =
+ new Person(
+ null,
+ "Jane",
+ "Smith",
+ 18, // minimum valid age
+ "+9876543210",
+ "jane@example.com",
+ validCategory);
+
+ // Assert
+ assertEquals(18, person.getAge());
+ }
+ }
+
+ @Nested
+ @DisplayName("Person Creation - Invalid Cases")
+ class InvalidPersonCreation {
+
+ @Test
+ @DisplayName("Should throw exception when first name is null")
+ void shouldThrowExceptionWhenFirstNameIsNull() {
+ // Act & Assert
+ var exception =
+ assertThrows(
+ DomainException.class,
+ () ->
+ new Person(
+ 1L, null, "Doe", 25, "+1234567890", "john@example.com", validCategory));
+ assertTrue(exception.getMessage().contains("First name and last name cannot be null"));
+ }
+
+ @Test
+ @DisplayName("Should throw exception when last name is null")
+ void shouldThrowExceptionWhenLastNameIsNull() {
+ // Act & Assert
+ var exception =
+ assertThrows(
+ DomainException.class,
+ () ->
+ new Person(
+ 1L, "John", null, 25, "+1234567890", "john@example.com", validCategory));
+ assertTrue(exception.getMessage().contains("First name and last name cannot be null"));
+ }
+
+ @Test
+ @DisplayName("Should throw exception when age is less than 18")
+ void shouldThrowExceptionWhenAgeIsLessThan18() {
+ // Act & Assert
+ var exception =
+ assertThrows(
+ DomainException.class,
+ () ->
+ new Person(
+ 1L, "John", "Doe", 17, "+1234567890", "john@example.com", validCategory));
+ assertTrue(exception.getMessage().contains("Age cannot be less than 18"));
+ }
+
+ @Test
+ @DisplayName("Should throw exception when phone number is null")
+ void shouldThrowExceptionWhenPhoneIsNull() {
+ // Act & Assert
+ var exception =
+ assertThrows(
+ DomainException.class,
+ () -> new Person(1L, "John", "Doe", 25, null, "john@example.com", validCategory));
+ assertTrue(exception.getMessage().contains("Phone number cannot be null or empty"));
+ }
+
+ @Test
+ @DisplayName("Should throw exception when phone number is empty")
+ void shouldThrowExceptionWhenPhoneIsEmpty() {
+ // Act & Assert
+ var exception =
+ assertThrows(
+ DomainException.class,
+ () -> new Person(1L, "John", "Doe", 25, "", "john@example.com", validCategory));
+ assertTrue(exception.getMessage().contains("Phone number cannot be null or empty"));
+ }
+
+ @Test
+ @DisplayName("Should throw exception when email is null")
+ void shouldThrowExceptionWhenEmailIsNull() {
+ // Act & Assert
+ var exception =
+ assertThrows(
+ DomainException.class,
+ () -> new Person(1L, "John", "Doe", 25, "+1234567890", null, validCategory));
+ assertTrue(exception.getMessage().contains("Email cannot be null or empty"));
+ }
+
+ @Test
+ @DisplayName("Should throw exception when email is empty")
+ void shouldThrowExceptionWhenEmailIsEmpty() {
+ // Act & Assert
+ var exception =
+ assertThrows(
+ DomainException.class,
+ () -> new Person(1L, "John", "Doe", 25, "+1234567890", "", validCategory));
+ assertTrue(exception.getMessage().contains("Email cannot be null or empty"));
+ }
+
+ @Test
+ @DisplayName("Should throw exception when category is null")
+ void shouldThrowExceptionWhenCategoryIsNull() {
+ // Act & Assert
+ var exception =
+ assertThrows(
+ DomainException.class,
+ () -> new Person(1L, "John", "Doe", 25, "+1234567890", "john@example.com", null));
+ assertTrue(exception.getMessage().contains("Category cannot be null or empty"));
+ }
+ }
+}
diff --git a/onion-architecture/etc/onion-architecture.png b/onion-architecture/etc/onion-architecture.png
new file mode 100644
index 000000000000..82f34e3d7fd1
Binary files /dev/null and b/onion-architecture/etc/onion-architecture.png differ
diff --git a/onion-architecture/etc/onion-architecture.puml b/onion-architecture/etc/onion-architecture.puml
new file mode 100644
index 000000000000..0e00429de2e6
--- /dev/null
+++ b/onion-architecture/etc/onion-architecture.puml
@@ -0,0 +1,237 @@
+@startuml onion-architecture
+
+skinparam packageStyle rectangle
+skinparam shadowing false
+skinparam defaultFontName Arial
+skinparam defaultFontSize 11
+skinparam classAttributeIconSize 0
+skinparam ArrowColor #444444
+skinparam ArrowThickness 1.2
+
+skinparam package {
+ BackgroundColor<> #FFF9C4
+ BorderColor<> #F9A825
+ FontColor<> #5D4037
+ FontStyle<> bold
+}
+
+skinparam package {
+ BackgroundColor<> #C8E6C9
+ BorderColor<> #388E3C
+ FontColor<> #1B5E20
+ FontStyle<> bold
+}
+
+skinparam package {
+ BackgroundColor<> #BBDEFB
+ BorderColor<> #1976D2
+ FontColor<> #0D47A1
+ FontStyle<> bold
+}
+
+' ─────────────────────────────────────────────────────────────────────────────
+' DOMAIN LAYER (innermost)
+' ─────────────────────────────────────────────────────────────────────────────
+package "Domain Layer" <> {
+
+ package "model" {
+ class Person {
+ - id : Long
+ - firstName : String
+ - lastName : String
+ - age : int
+ - phoneNumber : String
+ - email : String
+ - category : Category
+ + getId() : Long
+ + getFirstName() : String
+ + getLastName() : String
+ + getAge() : int
+ + getPhoneNumber() : String
+ + getEmail() : String
+ + getCategory() : Category
+ }
+
+ class Category {
+ - id : Long
+ - type : String
+ + getId() : Long
+ + getType() : String
+ }
+ }
+
+ package "repository" {
+ interface PersonRepository {
+ + findById(id : Long) : Optional
+ + findByFirstName(firstName : String) : Optional
+ + findByLastName(lastName : String) : Optional
+ + findAll() : Optional>
+ + save(person : Person) : Person
+ + deleteById(id : Long) : boolean
+ }
+ }
+
+ package "exception" {
+ class DomainException {
+ + DomainException(message : String)
+ }
+ }
+}
+
+' ─────────────────────────────────────────────────────────────────────────────
+' APPLICATION LAYER
+' ─────────────────────────────────────────────────────────────────────────────
+package "Application Layer" <> {
+
+ package "usecase" {
+ class SavePersonUseCase {
+ - repository : PersonRepository
+ + SavePersonUseCase(repository : PersonRepository)
+ + execute(command : SavePersonCommand) : PersonResponse
+ }
+
+ class GetPersonUseCase {
+ - repository : PersonRepository
+ + GetPersonUseCase(repository : PersonRepository)
+ + execute(id : Long) : PersonResponse
+ + executeAll() : List
+ }
+ }
+
+ package "dto" {
+ class SavePersonCommand <> {
+ firstName : String
+ lastName : String
+ age : int
+ phoneNumber : String
+ email : String
+ address : String
+ categoryId : Long
+ categoryType : String
+ }
+
+ class PersonResponse <> {
+ id : Long
+ firstName : String
+ lastName : String
+ age : int
+ phoneNumber : String
+ email : String
+ categoryId : Long
+ categoryType : String
+ }
+ }
+}
+
+' ─────────────────────────────────────────────────────────────────────────────
+' INFRASTRUCTURE LAYER (outermost)
+' ─────────────────────────────────────────────────────────────────────────────
+package "Infrastructure Layer" <> {
+
+ package "web" {
+ class PersonController {
+ - savePersonUseCase : SavePersonUseCase
+ - getPersonUseCase : GetPersonUseCase
+ + getPerson(id : Long) : ResponseEntity
+ + getAllPersons() : ResponseEntity>
+ + savePerson(command : SavePersonCommand) : ResponseEntity
+ }
+ }
+
+ package "persistence" {
+ class PersonRepositoryAdapter {
+ - repository : SpringDataPersonRepository
+ + findById(id : Long) : Optional
+ + findByFirstName(firstName : String) : Optional
+ + findByLastName(lastName : String) : Optional
+ + findAll() : Optional>
+ + save(person : Person) : Person
+ + deleteById(id : Long) : boolean
+ }
+
+ interface SpringDataPersonRepository {
+ + findByFirstName(firstName : String) : Optional
+ + findByLastName(lastName : String) : Optional
+ }
+
+ class JpaPersonEntity {
+ - id : Long
+ - firstName : String
+ - lastName : String
+ - age : int
+ - phoneNumber : String
+ - email : String
+ - category : JpaCategoryEntity
+ }
+
+ class JpaCategoryEntity {
+ - id : Long
+ - type : String
+ }
+ }
+
+ package "config" {
+ class ApplicationConfig <<@Configuration>> {
+ + savePersonUseCase(repo : PersonRepository) : SavePersonUseCase
+ + getPersonUseCase(repo : PersonRepository) : GetPersonUseCase
+ }
+
+ class Application <<@SpringBootApplication>> {
+ + main(args : String[])
+ }
+ }
+}
+
+' ─────────────────────────────────────────────────────────────────────────────
+' DOMAIN: internal relationships
+' ─────────────────────────────────────────────────────────────────────────────
+Person "1" *-- "1" Category : has
+Person ..> DomainException : <>
+Category ..> DomainException : <>
+
+' ─────────────────────────────────────────────────────────────────────────────
+' APPLICATION → DOMAIN
+' ─────────────────────────────────────────────────────────────────────────────
+SavePersonUseCase --> PersonRepository : uses
+SavePersonUseCase ..> Person : creates
+SavePersonUseCase ..> Category : creates
+SavePersonUseCase ..> PersonResponse : returns
+SavePersonUseCase ..> SavePersonCommand : receives
+
+GetPersonUseCase --> PersonRepository : uses
+GetPersonUseCase ..> PersonResponse : returns
+
+' ─────────────────────────────────────────────────────────────────────────────
+' INFRASTRUCTURE → APPLICATION / DOMAIN
+' ─────────────────────────────────────────────────────────────────────────────
+PersonController --> SavePersonUseCase : delegates to
+PersonController --> GetPersonUseCase : delegates to
+PersonController ..> DomainException : catches
+
+PersonRepositoryAdapter ..|> PersonRepository : implements
+PersonRepositoryAdapter --> SpringDataPersonRepository : uses
+PersonRepositoryAdapter ..> JpaPersonEntity : maps
+PersonRepositoryAdapter ..> JpaCategoryEntity : maps
+PersonRepositoryAdapter ..> Person : maps to / from
+PersonRepositoryAdapter ..> Category : maps to / from
+
+SpringDataPersonRepository --> JpaPersonEntity : manages
+JpaPersonEntity "1" *-- "1" JpaCategoryEntity : contains
+
+ApplicationConfig ..> SavePersonUseCase : <<@Bean>>
+ApplicationConfig ..> GetPersonUseCase : <<@Bean>>
+ApplicationConfig --> PersonRepository : injects
+
+legend right
+ Layer dependency rule
+ Infrastructure → Application → Domain
+ Outer layers depend on inner layers.
+ Domain has NO external dependencies.
+ ----
+ â– Domain Layer
+ â– Application Layer
+ â– Infrastructure Layer
+endlegend
+
+@enduml
+
diff --git a/onion-architecture/etc/postman/onion-architecture.postman_collection.json b/onion-architecture/etc/postman/onion-architecture.postman_collection.json
new file mode 100644
index 000000000000..ed0cbd5c58f5
--- /dev/null
+++ b/onion-architecture/etc/postman/onion-architecture.postman_collection.json
@@ -0,0 +1,83 @@
+{
+ "info": {
+ "_postman_id": "79aca374-7080-46c1-938f-89b327a84d6a",
+ "name": "onion-architecture",
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
+ "_exporter_id": "51221021",
+ "_collection_link": "https://go.postman.co/collection/51221021-79aca374-7080-46c1-938f-89b327a84d6a?source=collection_link"
+ },
+ "item": [
+ {
+ "name": "savePerson",
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\r\n \"number1\": 5,\r\n \"number2\": 3,\r\n \"operation\": \"multiply\"\r\n }"
+ },
+ "url": {
+ "raw": "http://localhost:8080/api/persons",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "api",
+ "persons"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "getPersons",
+ "protocolProfileBehavior": {
+ "disableBodyPruning": true
+ },
+ "request": {
+ "method": "GET",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\r\n \"number1\": 5,\r\n \"number2\": 3,\r\n \"operation\": \"multiply\"\r\n }"
+ },
+ "url": {
+ "raw": "http://localhost:8080/api/persons",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "api",
+ "persons"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "getPersonById",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8080/api/persons/1",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "api",
+ "persons",
+ "1"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/onion-architecture/infrastructure/pom.xml b/onion-architecture/infrastructure/pom.xml
new file mode 100644
index 000000000000..d26e2a7ef69d
--- /dev/null
+++ b/onion-architecture/infrastructure/pom.xml
@@ -0,0 +1,87 @@
+
+
+
+ 4.0.0
+
+ com.iluwatar
+ onion-architecture
+ 1.26.0-SNAPSHOT
+
+ infrastructure
+ Infrastructure
+
+
+
+ com.iluwatar
+ domain
+ ${project.version}
+
+
+
+ com.iluwatar
+ application
+ ${project.version}
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+
+ com.h2database
+ h2
+ runtime
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/onion-architecture/infrastructure/src/main/java/com/iluwatar/onion/infrastructure/Application.java b/onion-architecture/infrastructure/src/main/java/com/iluwatar/onion/infrastructure/Application.java
new file mode 100644
index 000000000000..c86c95740599
--- /dev/null
+++ b/onion-architecture/infrastructure/src/main/java/com/iluwatar/onion/infrastructure/Application.java
@@ -0,0 +1,38 @@
+/*
+ *
+ * * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ * *
+ * * The MIT License
+ * * Copyright © 2014-2022 Ilkka Seppälä
+ * *
+ * * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * * of this software and associated documentation files (the "Software"), to deal
+ * * in the Software without restriction, including without limitation the rights
+ * * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * * copies of the Software, and to permit persons to whom the Software is
+ * * furnished to do so, subject to the following conditions:
+ * *
+ * * The above copyright notice and this permission notice shall be included in
+ * * all copies or substantial portions of the Software.
+ * *
+ * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * * THE SOFTWARE.
+ *
+ */
+package com.iluwatar.onion.infrastructure;
+
+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);
+ }
+}
diff --git a/onion-architecture/infrastructure/src/main/java/com/iluwatar/onion/infrastructure/config/ApplicationConfig.java b/onion-architecture/infrastructure/src/main/java/com/iluwatar/onion/infrastructure/config/ApplicationConfig.java
new file mode 100644
index 000000000000..7794c91e3bcf
--- /dev/null
+++ b/onion-architecture/infrastructure/src/main/java/com/iluwatar/onion/infrastructure/config/ApplicationConfig.java
@@ -0,0 +1,47 @@
+/*
+ *
+ * * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ * *
+ * * The MIT License
+ * * Copyright © 2014-2022 Ilkka Seppälä
+ * *
+ * * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * * of this software and associated documentation files (the "Software"), to deal
+ * * in the Software without restriction, including without limitation the rights
+ * * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * * copies of the Software, and to permit persons to whom the Software is
+ * * furnished to do so, subject to the following conditions:
+ * *
+ * * The above copyright notice and this permission notice shall be included in
+ * * all copies or substantial portions of the Software.
+ * *
+ * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * * THE SOFTWARE.
+ *
+ */
+package com.iluwatar.onion.infrastructure.config;
+
+import com.iluwatar.onion.application.usecase.GetPersonUseCase;
+import com.iluwatar.onion.application.usecase.SavePersonUseCase;
+import com.iluwatar.onion.domain.repository.PersonRepository;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class ApplicationConfig {
+
+ @Bean
+ public SavePersonUseCase savePersonUseCase(PersonRepository repository) {
+ return new SavePersonUseCase(repository);
+ }
+
+ @Bean
+ public GetPersonUseCase getPersonUseCase(PersonRepository repository) {
+ return new GetPersonUseCase(repository);
+ }
+}
diff --git a/onion-architecture/infrastructure/src/main/java/com/iluwatar/onion/infrastructure/persistence/JpaCategoryEntity.java b/onion-architecture/infrastructure/src/main/java/com/iluwatar/onion/infrastructure/persistence/JpaCategoryEntity.java
new file mode 100644
index 000000000000..aafd242992be
--- /dev/null
+++ b/onion-architecture/infrastructure/src/main/java/com/iluwatar/onion/infrastructure/persistence/JpaCategoryEntity.java
@@ -0,0 +1,55 @@
+/*
+ *
+ * * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ * *
+ * * The MIT License
+ * * Copyright © 2014-2022 Ilkka Seppälä
+ * *
+ * * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * * of this software and associated documentation files (the "Software"), to deal
+ * * in the Software without restriction, including without limitation the rights
+ * * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * * copies of the Software, and to permit persons to whom the Software is
+ * * furnished to do so, subject to the following conditions:
+ * *
+ * * The above copyright notice and this permission notice shall be included in
+ * * all copies or substantial portions of the Software.
+ * *
+ * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * * THE SOFTWARE.
+ *
+ */
+package com.iluwatar.onion.infrastructure.persistence;
+
+import jakarta.persistence.*;
+
+@Entity
+@Table(name = "category")
+public class JpaCategoryEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ private String type;
+
+ public JpaCategoryEntity() {}
+
+ public JpaCategoryEntity(Long id, String type) {
+ this.id = id;
+ this.type = type;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public String getType() {
+ return type;
+ }
+}
diff --git a/onion-architecture/infrastructure/src/main/java/com/iluwatar/onion/infrastructure/persistence/JpaPersonEntity.java b/onion-architecture/infrastructure/src/main/java/com/iluwatar/onion/infrastructure/persistence/JpaPersonEntity.java
new file mode 100644
index 000000000000..bf71b5306c92
--- /dev/null
+++ b/onion-architecture/infrastructure/src/main/java/com/iluwatar/onion/infrastructure/persistence/JpaPersonEntity.java
@@ -0,0 +1,93 @@
+/*
+ *
+ * * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ * *
+ * * The MIT License
+ * * Copyright © 2014-2022 Ilkka Seppälä
+ * *
+ * * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * * of this software and associated documentation files (the "Software"), to deal
+ * * in the Software without restriction, including without limitation the rights
+ * * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * * copies of the Software, and to permit persons to whom the Software is
+ * * furnished to do so, subject to the following conditions:
+ * *
+ * * The above copyright notice and this permission notice shall be included in
+ * * all copies or substantial portions of the Software.
+ * *
+ * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * * THE SOFTWARE.
+ *
+ */
+package com.iluwatar.onion.infrastructure.persistence;
+
+import jakarta.persistence.*;
+
+@Entity
+@Table(name = "person")
+public class JpaPersonEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ private String firstName;
+ private String lastName;
+ private int age;
+ private String phoneNumber;
+ private String email;
+
+ @ManyToOne private JpaCategoryEntity category;
+
+ public JpaPersonEntity() {}
+
+ public JpaPersonEntity(
+ Long id,
+ String firstName,
+ String lastName,
+ int age,
+ String phoneNumber,
+ String email,
+ JpaCategoryEntity category) {
+ this.id = id;
+ this.firstName = firstName;
+ this.lastName = lastName;
+ this.age = age;
+ this.phoneNumber = phoneNumber;
+ this.email = email;
+ this.category = category;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public int getAge() {
+ return age;
+ }
+
+ public String getPhoneNumber() {
+ return phoneNumber;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public JpaCategoryEntity getCategory() {
+ return category;
+ }
+}
diff --git a/onion-architecture/infrastructure/src/main/java/com/iluwatar/onion/infrastructure/persistence/PersonRepositoryAdapter.java b/onion-architecture/infrastructure/src/main/java/com/iluwatar/onion/infrastructure/persistence/PersonRepositoryAdapter.java
new file mode 100644
index 000000000000..4c4aa267493a
--- /dev/null
+++ b/onion-architecture/infrastructure/src/main/java/com/iluwatar/onion/infrastructure/persistence/PersonRepositoryAdapter.java
@@ -0,0 +1,102 @@
+/*
+ *
+ * * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ * *
+ * * The MIT License
+ * * Copyright © 2014-2022 Ilkka Seppälä
+ * *
+ * * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * * of this software and associated documentation files (the "Software"), to deal
+ * * in the Software without restriction, including without limitation the rights
+ * * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * * copies of the Software, and to permit persons to whom the Software is
+ * * furnished to do so, subject to the following conditions:
+ * *
+ * * The above copyright notice and this permission notice shall be included in
+ * * all copies or substantial portions of the Software.
+ * *
+ * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * * THE SOFTWARE.
+ *
+ */
+package com.iluwatar.onion.infrastructure.persistence;
+
+import com.iluwatar.onion.domain.model.Category;
+import com.iluwatar.onion.domain.model.Person;
+import com.iluwatar.onion.domain.repository.PersonRepository;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public class PersonRepositoryAdapter implements PersonRepository {
+
+ private final SpringDataPersonRepository repository;
+
+ public PersonRepositoryAdapter(SpringDataPersonRepository repository) {
+ this.repository = repository;
+ }
+
+ @Override
+ public Optional findById(Long id) {
+ return repository.findById(id).map(this::mapToDomain);
+ }
+
+ @Override
+ public Optional findByFirstName(String firstName) {
+ return repository.findByFirstName(firstName).map(this::mapToDomain);
+ }
+
+ @Override
+ public Optional findByLastName(String lastName) {
+ return repository.findByLastName(lastName).map(this::mapToDomain);
+ }
+
+ @Override
+ public Optional> findAll() {
+ return repository.findAll().stream()
+ .map(this::mapToDomain)
+ .collect(Collectors.collectingAndThen(Collectors.toList(), Optional::of));
+ }
+
+ @Override
+ public Person save(Person person) {
+ JpaPersonEntity entity = mapToEntity(person);
+ JpaPersonEntity savedEntity = repository.save(entity);
+ return mapToDomain(savedEntity);
+ }
+
+ @Override
+ public boolean deleteById(Long id) {
+ repository.deleteById(id);
+ return repository.findById(id).isEmpty();
+ }
+
+ private JpaPersonEntity mapToEntity(Person person) {
+ return new JpaPersonEntity(
+ person.getId(),
+ person.getFirstName(),
+ person.getLastName(),
+ person.getAge(),
+ person.getPhoneNumber(),
+ person.getEmail(),
+ new JpaCategoryEntity(person.getCategory().getId(), person.getCategory().getType()));
+ }
+
+ private Person mapToDomain(JpaPersonEntity entity) {
+ return new Person(
+ entity.getId(),
+ entity.getFirstName(),
+ entity.getLastName(),
+ entity.getAge(),
+ entity.getPhoneNumber(),
+ entity.getEmail(),
+ new Category(entity.getCategory().getId(), entity.getCategory().getType()));
+ }
+}
diff --git a/onion-architecture/infrastructure/src/main/java/com/iluwatar/onion/infrastructure/persistence/SpringDataPersonRepository.java b/onion-architecture/infrastructure/src/main/java/com/iluwatar/onion/infrastructure/persistence/SpringDataPersonRepository.java
new file mode 100644
index 000000000000..35f86515c70b
--- /dev/null
+++ b/onion-architecture/infrastructure/src/main/java/com/iluwatar/onion/infrastructure/persistence/SpringDataPersonRepository.java
@@ -0,0 +1,37 @@
+/*
+ *
+ * * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ * *
+ * * The MIT License
+ * * Copyright © 2014-2022 Ilkka Seppälä
+ * *
+ * * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * * of this software and associated documentation files (the "Software"), to deal
+ * * in the Software without restriction, including without limitation the rights
+ * * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * * copies of the Software, and to permit persons to whom the Software is
+ * * furnished to do so, subject to the following conditions:
+ * *
+ * * The above copyright notice and this permission notice shall be included in
+ * * all copies or substantial portions of the Software.
+ * *
+ * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * * THE SOFTWARE.
+ *
+ */
+package com.iluwatar.onion.infrastructure.persistence;
+
+import java.util.Optional;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface SpringDataPersonRepository extends JpaRepository {
+
+ Optional findByFirstName(String firstName);
+
+ Optional findByLastName(String lastName);
+}
diff --git a/onion-architecture/infrastructure/src/main/java/com/iluwatar/onion/infrastructure/web/PersonController.java b/onion-architecture/infrastructure/src/main/java/com/iluwatar/onion/infrastructure/web/PersonController.java
new file mode 100644
index 000000000000..417e749f41a6
--- /dev/null
+++ b/onion-architecture/infrastructure/src/main/java/com/iluwatar/onion/infrastructure/web/PersonController.java
@@ -0,0 +1,72 @@
+/*
+ *
+ * * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ * *
+ * * The MIT License
+ * * Copyright © 2014-2022 Ilkka Seppälä
+ * *
+ * * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * * of this software and associated documentation files (the "Software"), to deal
+ * * in the Software without restriction, including without limitation the rights
+ * * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * * copies of the Software, and to permit persons to whom the Software is
+ * * furnished to do so, subject to the following conditions:
+ * *
+ * * The above copyright notice and this permission notice shall be included in
+ * * all copies or substantial portions of the Software.
+ * *
+ * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * * THE SOFTWARE.
+ *
+ */
+package com.iluwatar.onion.infrastructure.web;
+
+import com.iluwatar.onion.application.dto.PersonResponse;
+import com.iluwatar.onion.application.dto.SavePersonCommand;
+import com.iluwatar.onion.application.usecase.GetPersonUseCase;
+import com.iluwatar.onion.application.usecase.SavePersonUseCase;
+import com.iluwatar.onion.domain.exception.DomainException;
+import java.util.List;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api")
+public class PersonController {
+
+ private final SavePersonUseCase savePersonUseCase;
+ private final GetPersonUseCase getPersonUseCase;
+
+ public PersonController(SavePersonUseCase savePersonUseCase, GetPersonUseCase getPersonUseCase) {
+ this.savePersonUseCase = savePersonUseCase;
+ this.getPersonUseCase = getPersonUseCase;
+ }
+
+ @GetMapping("/persons/{id}")
+ public ResponseEntity getPerson(@PathVariable("id") Long id) {
+ var person = getPersonUseCase.execute(id);
+ return ResponseEntity.ok(person);
+ }
+
+ @GetMapping("/persons")
+ public ResponseEntity> getAllPersons() {
+ var persons = getPersonUseCase.executeAll();
+ return ResponseEntity.ok(persons);
+ }
+
+ @PostMapping("/persons")
+ public ResponseEntity savePerson(@RequestBody SavePersonCommand command) {
+ try {
+ var savedPerson = savePersonUseCase.execute(command);
+ return ResponseEntity.status(HttpStatus.OK).body(savedPerson);
+ } catch (DomainException e) {
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
+ }
+ }
+}
diff --git a/onion-architecture/infrastructure/src/main/resources/application.properties b/onion-architecture/infrastructure/src/main/resources/application.properties
new file mode 100644
index 000000000000..6a3ed156f558
--- /dev/null
+++ b/onion-architecture/infrastructure/src/main/resources/application.properties
@@ -0,0 +1,19 @@
+# You can also specify the server port in the application.properties file.
+# Uncomment the following line and set the desired port number to change
+# the default port (8080) to a different value.
+#server.port=8080
+
+spring.datasource.url=jdbc:h2:mem:testdb
+spring.datasource.driverClassName=org.h2.Driver
+spring.datasource.username=sa
+spring.datasource.password=password
+spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
+
+# Force Spring Boot to always run SQL initialization scripts on startup
+spring.sql.init.mode=always
+
+# If using Spring Data JPA / Hibernate, defer data insertion until AFTER entities are generated
+spring.jpa.defer-datasource-initialization=true
+
+# Keep Hibernate's DDL generation active (or set to 'update')
+spring.jpa.hibernate.ddl-auto=create-drop
diff --git a/onion-architecture/infrastructure/src/main/resources/data.sql b/onion-architecture/infrastructure/src/main/resources/data.sql
new file mode 100644
index 000000000000..6291dd984e31
--- /dev/null
+++ b/onion-architecture/infrastructure/src/main/resources/data.sql
@@ -0,0 +1,4 @@
+INSERT INTO category (id, type) VALUES (1, 'Teenage');
+INSERT INTO category (id, type) VALUES (2, 'Young Adult');
+INSERT INTO category (id, type) VALUES (3, 'Adult');
+INSERT INTO category (id, type) VALUES (4, 'Senior');
\ No newline at end of file
diff --git a/onion-architecture/infrastructure/src/test/java/com/iluwatar/onion/infrastructure/persistence/PersonRepositoryAdapterTest.java b/onion-architecture/infrastructure/src/test/java/com/iluwatar/onion/infrastructure/persistence/PersonRepositoryAdapterTest.java
new file mode 100644
index 000000000000..49fd2816f172
--- /dev/null
+++ b/onion-architecture/infrastructure/src/test/java/com/iluwatar/onion/infrastructure/persistence/PersonRepositoryAdapterTest.java
@@ -0,0 +1,332 @@
+/*
+ *
+ * * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ * *
+ * * The MIT License
+ * * Copyright © 2014-2022 Ilkka Seppälä
+ * *
+ * * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * * of this software and associated documentation files (the "Software"), to deal
+ * * in the Software without restriction, including without limitation the rights
+ * * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * * copies of the Software, and to permit persons to whom the Software is
+ * * furnished to do so, subject to the following conditions:
+ * *
+ * * The above copyright notice and this permission notice shall be included in
+ * * all copies or substantial portions of the Software.
+ * *
+ * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * * THE SOFTWARE.
+ *
+ */
+package com.iluwatar.onion.infrastructure.persistence;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+import com.iluwatar.onion.domain.model.Category;
+import com.iluwatar.onion.domain.model.Person;
+import java.util.List;
+import java.util.Optional;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class PersonRepositoryAdapterTest {
+
+ @Mock private SpringDataPersonRepository springDataRepository;
+
+ @Captor private ArgumentCaptor entityCaptor;
+
+ private PersonRepositoryAdapter personRepositoryAdapter;
+ private JpaPersonEntity jpaPersonEntity;
+
+ @BeforeEach
+ void setUp() {
+ personRepositoryAdapter = new PersonRepositoryAdapter(springDataRepository);
+
+ // Setup test data
+ var jpaCategory = new JpaCategoryEntity(1L, "Professional");
+ jpaPersonEntity =
+ new JpaPersonEntity(
+ 1L, "John", "Doe", 25, "+1234567890", "john.doe@example.com", jpaCategory);
+ }
+
+ @Nested
+ @DisplayName("Find By ID")
+ class FindById {
+
+ @Test
+ @DisplayName("Should return Person when entity exists")
+ void shouldReturnPersonWhenEntityExists() {
+ // Arrange
+ when(springDataRepository.findById(1L)).thenReturn(Optional.of(jpaPersonEntity));
+
+ // Act
+ var result = personRepositoryAdapter.findById(1L);
+
+ // Assert
+ assertTrue(result.isPresent());
+ Person person = result.get();
+ assertEquals(1L, person.getId());
+ assertEquals("John", person.getFirstName());
+ assertEquals("Doe", person.getLastName());
+ assertEquals(25, person.getAge());
+ assertEquals("+1234567890", person.getPhoneNumber());
+ assertEquals("john.doe@example.com", person.getEmail());
+ assertEquals(1L, person.getCategory().getId());
+ assertEquals("Professional", person.getCategory().getType());
+
+ verify(springDataRepository, times(1)).findById(1L);
+ }
+
+ @Test
+ @DisplayName("Should return empty Optional when entity not found")
+ void shouldReturnEmptyWhenEntityNotFound() {
+ // Arrange
+ when(springDataRepository.findById(999L)).thenReturn(Optional.empty());
+
+ // Act
+ var result = personRepositoryAdapter.findById(999L);
+
+ // Assert
+ assertTrue(result.isEmpty());
+ verify(springDataRepository, times(1)).findById(999L);
+ }
+ }
+
+ @Nested
+ @DisplayName("Find By First Name")
+ class FindByFirstName {
+
+ @Test
+ @DisplayName("Should return Person when entity with first name exists")
+ void shouldReturnPersonWhenEntityExists() {
+ // Arrange
+ when(springDataRepository.findByFirstName("John")).thenReturn(Optional.of(jpaPersonEntity));
+
+ // Act
+ var result = personRepositoryAdapter.findByFirstName("John");
+
+ // Assert
+ assertTrue(result.isPresent());
+ assertEquals("John", result.get().getFirstName());
+ verify(springDataRepository, times(1)).findByFirstName("John");
+ }
+
+ @Test
+ @DisplayName("Should return empty Optional when no entity found")
+ void shouldReturnEmptyWhenNotFound() {
+ // Arrange
+ when(springDataRepository.findByFirstName("Unknown")).thenReturn(Optional.empty());
+
+ // Act
+ var result = personRepositoryAdapter.findByFirstName("Unknown");
+
+ // Assert
+ assertTrue(result.isEmpty());
+ verify(springDataRepository, times(1)).findByFirstName("Unknown");
+ }
+ }
+
+ @Nested
+ @DisplayName("Find By Last Name")
+ class FindByLastName {
+
+ @Test
+ @DisplayName("Should return Person when entity with last name exists")
+ void shouldReturnPersonWhenEntityExists() {
+ // Arrange
+ when(springDataRepository.findByLastName("Doe")).thenReturn(Optional.of(jpaPersonEntity));
+
+ // Act
+ var result = personRepositoryAdapter.findByLastName("Doe");
+
+ // Assert
+ assertTrue(result.isPresent());
+ assertEquals("Doe", result.get().getLastName());
+ verify(springDataRepository, times(1)).findByLastName("Doe");
+ }
+ }
+
+ @Nested
+ @DisplayName("Find All")
+ class FindAll {
+
+ @Test
+ @DisplayName("Should return list of Persons when entities exist")
+ void shouldReturnListOfPersons() {
+ // Arrange
+ var category2 = new JpaCategoryEntity(2L, "Personal");
+ var person2 =
+ new JpaPersonEntity(
+ 2L, "Jane", "Smith", 30, "+9876543210", "jane@example.com", category2);
+
+ when(springDataRepository.findAll()).thenReturn(List.of(jpaPersonEntity, person2));
+
+ // Act
+ var result = personRepositoryAdapter.findAll();
+
+ // Assert
+ assertTrue(result.isPresent());
+ var persons = result.get();
+ assertEquals(2, persons.size());
+ assertEquals("John", persons.get(0).getFirstName());
+ assertEquals("Jane", persons.get(1).getFirstName());
+
+ verify(springDataRepository, times(1)).findAll();
+ }
+
+ @Test
+ @DisplayName("Should return empty list when no entities exist")
+ void shouldReturnEmptyListWhenNoEntities() {
+ // Arrange
+ when(springDataRepository.findAll()).thenReturn(List.of());
+
+ // Act
+ var result = personRepositoryAdapter.findAll();
+
+ // Assert
+ assertTrue(result.isPresent());
+ assertTrue(result.get().isEmpty());
+ verify(springDataRepository, times(1)).findAll();
+ }
+ }
+
+ @Nested
+ @DisplayName("Save")
+ class Save {
+
+ @Test
+ @DisplayName("Should save person and return domain model")
+ void shouldSavePersonAndReturnDomainModel() {
+ // Arrange
+ var personToSave =
+ new Person(
+ null, // New person without ID
+ "Jane",
+ "Smith",
+ 28,
+ "+1111111111",
+ "jane@example.com",
+ new Category(2L, "Personal"));
+
+ var savedCategory = new JpaCategoryEntity(2L, "Personal");
+ var savedEntity =
+ new JpaPersonEntity(
+ 100L, // ID assigned by database
+ "Jane",
+ "Smith",
+ 28,
+ "+1111111111",
+ "jane@example.com",
+ savedCategory);
+
+ when(springDataRepository.save(any(JpaPersonEntity.class))).thenReturn(savedEntity);
+
+ // Act
+ var result = personRepositoryAdapter.save(personToSave);
+
+ // Assert
+ assertNotNull(result);
+ assertEquals(100L, result.getId());
+ assertEquals("Jane", result.getFirstName());
+ assertEquals("Smith", result.getLastName());
+ assertEquals(28, result.getAge());
+
+ verify(springDataRepository, times(1)).save(any(JpaPersonEntity.class));
+ }
+
+ @Test
+ @DisplayName("Should correctly map domain model to JPA entity")
+ void shouldCorrectlyMapDomainToEntity() {
+ // Arrange
+ var personToSave =
+ new Person(
+ null,
+ "Bob",
+ "Johnson",
+ 35,
+ "+2222222222",
+ "bob@example.com",
+ new Category(3L, "Business"));
+
+ var savedEntity =
+ new JpaPersonEntity(
+ 200L,
+ "Bob",
+ "Johnson",
+ 35,
+ "+2222222222",
+ "bob@example.com",
+ new JpaCategoryEntity(3L, "Business"));
+
+ when(springDataRepository.save(any(JpaPersonEntity.class))).thenReturn(savedEntity);
+
+ // Act
+ personRepositoryAdapter.save(personToSave);
+
+ // Assert - Verify what was passed to Spring Data repository
+ verify(springDataRepository).save(entityCaptor.capture());
+ JpaPersonEntity capturedEntity = entityCaptor.getValue();
+
+ assertEquals("Bob", capturedEntity.getFirstName());
+ assertEquals("Johnson", capturedEntity.getLastName());
+ assertEquals(35, capturedEntity.getAge());
+ assertEquals("+2222222222", capturedEntity.getPhoneNumber());
+ assertEquals("bob@example.com", capturedEntity.getEmail());
+ assertEquals(3L, capturedEntity.getCategory().getId());
+ assertEquals("Business", capturedEntity.getCategory().getType());
+ }
+ }
+
+ @Nested
+ @DisplayName("Delete By ID")
+ class DeleteById {
+
+ @Test
+ @DisplayName("Should return true when person is successfully deleted")
+ void shouldReturnTrueWhenDeleted() {
+ // Arrange
+ doNothing().when(springDataRepository).deleteById(1L);
+ when(springDataRepository.findById(1L)).thenReturn(Optional.empty());
+
+ // Act
+ boolean result = personRepositoryAdapter.deleteById(1L);
+
+ // Assert
+ assertTrue(result);
+ verify(springDataRepository, times(1)).deleteById(1L);
+ verify(springDataRepository, times(1)).findById(1L);
+ }
+
+ @Test
+ @DisplayName("Should return false when person still exists after deletion")
+ void shouldReturnFalseWhenStillExists() {
+ // Arrange
+ doNothing().when(springDataRepository).deleteById(1L);
+ when(springDataRepository.findById(1L)).thenReturn(Optional.of(jpaPersonEntity));
+
+ // Act
+ boolean result = personRepositoryAdapter.deleteById(1L);
+
+ // Assert
+ assertFalse(result);
+ verify(springDataRepository, times(1)).deleteById(1L);
+ verify(springDataRepository, times(1)).findById(1L);
+ }
+ }
+}
diff --git a/onion-architecture/infrastructure/src/test/java/com/iluwatar/onion/infrastructure/web/PersonControllerTest.java b/onion-architecture/infrastructure/src/test/java/com/iluwatar/onion/infrastructure/web/PersonControllerTest.java
new file mode 100644
index 000000000000..9e0369b985c8
--- /dev/null
+++ b/onion-architecture/infrastructure/src/test/java/com/iluwatar/onion/infrastructure/web/PersonControllerTest.java
@@ -0,0 +1,348 @@
+/*
+ *
+ * * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ * *
+ * * The MIT License
+ * * Copyright © 2014-2022 Ilkka Seppälä
+ * *
+ * * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * * of this software and associated documentation files (the "Software"), to deal
+ * * in the Software without restriction, including without limitation the rights
+ * * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * * copies of the Software, and to permit persons to whom the Software is
+ * * furnished to do so, subject to the following conditions:
+ * *
+ * * The above copyright notice and this permission notice shall be included in
+ * * all copies or substantial portions of the Software.
+ * *
+ * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * * THE SOFTWARE.
+ *
+ */
+package com.iluwatar.onion.infrastructure.web;
+
+import static org.hamcrest.Matchers.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.iluwatar.onion.application.dto.PersonResponse;
+import com.iluwatar.onion.application.dto.SavePersonCommand;
+import com.iluwatar.onion.application.usecase.GetPersonUseCase;
+import com.iluwatar.onion.application.usecase.SavePersonUseCase;
+import com.iluwatar.onion.domain.exception.DomainException;
+import java.util.List;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.web.servlet.MockMvc;
+
+@WebMvcTest(PersonController.class)
+class PersonControllerTest {
+
+ @Autowired private MockMvc mockMvc;
+
+ @Autowired private ObjectMapper objectMapper;
+
+ @MockitoBean private SavePersonUseCase savePersonUseCase;
+
+ @MockitoBean private GetPersonUseCase getPersonUseCase;
+
+ @Nested
+ @DisplayName("GET /api/persons/{id}")
+ class GetPersonById {
+
+ @Test
+ @DisplayName("Should return person when person exists")
+ void shouldReturnPersonWhenExists() throws Exception {
+ // Arrange
+ var response =
+ new PersonResponse(
+ 1L, "John", "Doe", 25, "+1234567890", "john.doe@example.com", 1L, "Professional");
+
+ when(getPersonUseCase.execute(1L)).thenReturn(response);
+
+ // Act & Assert
+ mockMvc
+ .perform(get("/api/persons/1").contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("$.id").value(1))
+ .andExpect(jsonPath("$.firstName").value("John"))
+ .andExpect(jsonPath("$.lastName").value("Doe"))
+ .andExpect(jsonPath("$.age").value(25))
+ .andExpect(jsonPath("$.phoneNumber").value("+1234567890"))
+ .andExpect(jsonPath("$.email").value("john.doe@example.com"))
+ .andExpect(jsonPath("$.categoryId").value(1))
+ .andExpect(jsonPath("$.categoryType").value("Professional"));
+
+ verify(getPersonUseCase, times(1)).execute(1L);
+ }
+
+ @Test
+ @DisplayName("Should handle different person IDs")
+ void shouldHandleDifferentPersonIds() throws Exception {
+ // Arrange
+ var response =
+ new PersonResponse(
+ 42L, "Jane", "Smith", 30, "+9876543210", "jane@example.com", 2L, "Personal");
+
+ when(getPersonUseCase.execute(42L)).thenReturn(response);
+
+ // Act & Assert
+ mockMvc
+ .perform(get("/api/persons/42").contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.id").value(42))
+ .andExpect(jsonPath("$.firstName").value("Jane"));
+
+ verify(getPersonUseCase, times(1)).execute(42L);
+ }
+ }
+
+ @Nested
+ @DisplayName("GET /api/persons")
+ class GetAllPersons {
+
+ @Test
+ @DisplayName("Should return list of persons")
+ void shouldReturnListOfPersons() throws Exception {
+ // Arrange
+ var person1 =
+ new PersonResponse(
+ 1L, "John", "Doe", 25, "+1234567890", "john@example.com", 1L, "Professional");
+ var person2 =
+ new PersonResponse(
+ 2L, "Jane", "Smith", 30, "+9876543210", "jane@example.com", 2L, "Personal");
+
+ var persons = List.of(person1, person2);
+ when(getPersonUseCase.executeAll()).thenReturn(persons);
+
+ // Act & Assert
+ mockMvc
+ .perform(get("/api/persons").contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("$", hasSize(2)))
+ .andExpect(jsonPath("$[0].id").value(1))
+ .andExpect(jsonPath("$[0].firstName").value("John"))
+ .andExpect(jsonPath("$[1].id").value(2))
+ .andExpect(jsonPath("$[1].firstName").value("Jane"));
+
+ verify(getPersonUseCase, times(1)).executeAll();
+ }
+
+ @Test
+ @DisplayName("Should return empty list when no persons exist")
+ void shouldReturnEmptyListWhenNoPersons() throws Exception {
+ // Arrange
+ when(getPersonUseCase.executeAll()).thenReturn(List.of());
+
+ // Act & Assert
+ mockMvc
+ .perform(get("/api/persons").contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("$", hasSize(0)));
+
+ verify(getPersonUseCase, times(1)).executeAll();
+ }
+
+ @Test
+ @DisplayName("Should handle large list of persons")
+ void shouldHandleLargeListOfPersons() throws Exception {
+ // Arrange
+ var persons =
+ List.of(
+ new PersonResponse(1L, "Person1", "Last1", 25, "+1", "p1@example.com", 1L, "Cat1"),
+ new PersonResponse(2L, "Person2", "Last2", 26, "+2", "p2@example.com", 1L, "Cat1"),
+ new PersonResponse(3L, "Person3", "Last3", 27, "+3", "p3@example.com", 1L, "Cat1"));
+
+ when(getPersonUseCase.executeAll()).thenReturn(persons);
+
+ // Act & Assert
+ mockMvc
+ .perform(get("/api/persons").contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$", hasSize(3)));
+
+ verify(getPersonUseCase, times(1)).executeAll();
+ }
+ }
+
+ @Nested
+ @DisplayName("POST /api/persons")
+ class SavePerson {
+
+ @Test
+ @DisplayName("Should create person with valid data")
+ void shouldCreatePersonWithValidData() throws Exception {
+ // Arrange
+ var command =
+ new SavePersonCommand(
+ "John",
+ "Doe",
+ 25,
+ "+1234567890",
+ "john.doe@example.com",
+ "123 Main St",
+ 1L,
+ "Professional");
+
+ var response =
+ new PersonResponse(
+ 1L, "John", "Doe", 25, "+1234567890", "john.doe@example.com", 1L, "Professional");
+
+ when(savePersonUseCase.execute(any(SavePersonCommand.class))).thenReturn(response);
+
+ // Act & Assert
+ mockMvc
+ .perform(
+ post("/api/persons")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(command)))
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("$.id").value(1))
+ .andExpect(jsonPath("$.firstName").value("John"))
+ .andExpect(jsonPath("$.lastName").value("Doe"))
+ .andExpect(jsonPath("$.age").value(25))
+ .andExpect(jsonPath("$.phoneNumber").value("+1234567890"))
+ .andExpect(jsonPath("$.email").value("john.doe@example.com"))
+ .andExpect(jsonPath("$.categoryId").value(1))
+ .andExpect(jsonPath("$.categoryType").value("Professional"));
+
+ verify(savePersonUseCase, times(1)).execute(any(SavePersonCommand.class));
+ }
+
+ @Test
+ @DisplayName("Should handle validation errors from use case")
+ void shouldPropagateValidationErrors() throws Exception {
+ // Arrange
+ var command =
+ new SavePersonCommand(
+ "Young",
+ "Person",
+ 17, // Invalid age
+ "+1234567890",
+ "young@example.com",
+ "123 Main St",
+ 1L,
+ "Student");
+
+ when(savePersonUseCase.execute(any(SavePersonCommand.class)))
+ .thenThrow(new DomainException("Age cannot be less than 18"));
+
+ // Act & Assert
+ mockMvc
+ .perform(
+ post("/api/persons")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(command)))
+ .andExpect(status().is4xxClientError());
+
+ verify(savePersonUseCase, times(1)).execute(any(SavePersonCommand.class));
+ }
+
+ @Test
+ @DisplayName("Should handle malformed JSON")
+ void shouldHandleMalformedJson() throws Exception {
+ // Act & Assert
+ mockMvc
+ .perform(
+ post("/api/persons")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{invalid json}"))
+ .andExpect(status().isBadRequest());
+
+ verify(savePersonUseCase, never()).execute(any(SavePersonCommand.class));
+ }
+
+ @Test
+ @DisplayName("Should accept all required fields in command")
+ void shouldAcceptAllRequiredFields() throws Exception {
+ // Arrange
+ var command =
+ new SavePersonCommand(
+ "Complete",
+ "Person",
+ 30,
+ "+1111111111",
+ "complete@example.com",
+ "456 Oak Ave",
+ 5L,
+ "Business");
+
+ var response =
+ new PersonResponse(
+ 10L, "Complete", "Person", 30, "+1111111111", "complete@example.com", 5L, "Business");
+
+ when(savePersonUseCase.execute(any(SavePersonCommand.class))).thenReturn(response);
+
+ // Act & Assert
+ mockMvc
+ .perform(
+ post("/api/persons")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(command)))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.categoryType").value("Business"));
+
+ verify(savePersonUseCase, times(1)).execute(any(SavePersonCommand.class));
+ }
+ }
+
+ @Nested
+ @DisplayName("Controller Integration")
+ class ControllerIntegration {
+
+ @Test
+ @DisplayName("Should handle multiple requests sequentially")
+ void shouldHandleMultipleRequestsSequentially() throws Exception {
+ // Arrange
+ var getResponse =
+ new PersonResponse(
+ 1L, "John", "Doe", 25, "+1234567890", "john@example.com", 1L, "Professional");
+
+ var savePersonCommand =
+ new SavePersonCommand(
+ "Jane", "Smith", 30, "+9876543210", "jane@example.com", "456 Oak", 2L, "Personal");
+
+ var savePersonResponse =
+ new PersonResponse(
+ 2L, "Jane", "Smith", 30, "+9876543210", "jane@example.com", 2L, "Personal");
+
+ when(getPersonUseCase.execute(1L)).thenReturn(getResponse);
+ when(savePersonUseCase.execute(any(SavePersonCommand.class))).thenReturn(savePersonResponse);
+
+ // Act & Assert - GET request
+ mockMvc
+ .perform(get("/api/persons/1"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.firstName").value("John"));
+
+ // Act & Assert - POST request
+ mockMvc
+ .perform(
+ post("/api/persons")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(savePersonCommand)))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.firstName").value("Jane"));
+
+ verify(getPersonUseCase, times(1)).execute(1L);
+ verify(savePersonUseCase, times(1)).execute(any(SavePersonCommand.class));
+ }
+ }
+}
diff --git a/onion-architecture/pom.xml b/onion-architecture/pom.xml
new file mode 100644
index 000000000000..07df2a1224c1
--- /dev/null
+++ b/onion-architecture/pom.xml
@@ -0,0 +1,49 @@
+
+
+ 4.0.0
+
+ com.iluwatar
+ java-design-patterns
+ 1.26.0-SNAPSHOT
+
+ onion-architecture
+ pom
+
+
+ 5.10.0
+ 5.23.0
+ 3.24.2
+
+
+
+ domain
+ application
+ infrastructure
+
+
diff --git a/polling-publisher/pom.xml b/polling-publisher/pom.xml
index 2e5f7e8f35cb..1f69809390af 100644
--- a/polling-publisher/pom.xml
+++ b/polling-publisher/pom.xml
@@ -93,7 +93,7 @@
ch.qos.logback
logback-core
- 1.5.18
+ 1.5.34
ch.qos.logback
diff --git a/pom.xml b/pom.xml
index b856945f3b1e..46cec4ee61c4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -80,6 +80,7 @@
anti-corruption-layer
arrange-act-assert
async-method-invocation
+ backends-for-frontends
balking
bloc
bridge
@@ -106,7 +107,7 @@
converter
curiously-recurring-template-pattern
currying
- dao-factory
+ dao-factory
data-access-object
data-bus
data-locality
@@ -169,6 +170,7 @@
microservices-idempotent-consumer
microservices-log-aggregation
microservices-self-registration
+ microservices-messaging
model-view-controller
model-view-intent
model-view-presenter
@@ -208,6 +210,7 @@
resource-acquisition-is-initialization
retry
role-object
+ rule-engine
saga
separated-interface
serialized-entity
@@ -252,6 +255,8 @@
backpressure
actor-model
rate-limiting-pattern
+ fallback
+ onion-architecture
@@ -346,6 +351,11 @@
mockito-core
${mockito.version}
+
+ org.mockito
+ mockito-junit-jupiter
+ ${mockito.version}
+
org.mongodb
bson
@@ -444,6 +454,7 @@
src/test/resources/**
src/main/resources/**
checkstyle-suppressions.xml
+ **/*.ps1
@@ -484,7 +495,7 @@
com.diffplug.spotless
spotless-maven-plugin
- 2.44.4
+ 3.8.0
diff --git a/rate-limiting-pattern/pom.xml b/rate-limiting-pattern/pom.xml
index 517a02094a9b..370b59e7b283 100644
--- a/rate-limiting-pattern/pom.xml
+++ b/rate-limiting-pattern/pom.xml
@@ -59,7 +59,7 @@
com.diffplug.spotless
spotless-maven-plugin
- 2.44.2
+ 3.8.0
diff --git a/rule-engine/README.md b/rule-engine/README.md
new file mode 100644
index 000000000000..53106a71d524
--- /dev/null
+++ b/rule-engine/README.md
@@ -0,0 +1,199 @@
+---
+title: "Rule Engine Pattern in Java: Replacing Tangled Conditionals with Composable Rules"
+shortTitle: Rule Engine
+description: "Learn the Rule Engine design pattern in Java. Encapsulate each business rule as an independent object and let an engine evaluate and execute them against a shared context, replacing large nested if/else blocks with small, testable, composable rules."
+category: Behavioral
+language: en
+tag:
+ - Business
+ - Decoupling
+ - Domain
+ - Encapsulation
+ - Extensibility
+---
+
+## Intent of Rule Engine Design Pattern
+
+Encapsulate each business rule as an independent, self-contained object and let an engine evaluate and execute a collection of those rules against a shared context, so that decision logic can grow and change without rewriting a large nested conditional block.
+
+## Detailed Explanation of Rule Engine Pattern with Real-World Examples
+
+Real-world example
+
+> A bank decides whether to approve a loan by checking several independent criteria: the applicant must be old enough, earn enough, and have a good enough credit score. Instead of hard-coding one enormous condition, the bank keeps each criterion as a separate policy on a checklist. A clerk walks the whole checklist for every application, ticks off the criteria that pass, and writes down the ones that fail. Adding a new criterion means adding a line to the checklist, not rewriting the whole approval procedure.
+
+In plain words
+
+> The Rule Engine pattern turns each branch of a giant `if/else` into its own object and hands a collection of them to an engine that runs them all against the same input, collecting which passed and which failed.
+
+## Programmatic Example of Rule Engine Pattern in Java
+
+Each rule separates the **decision** (`evaluate`) from the **action** (`execute`). `evaluate` reports whether a rule's condition holds; `execute` performs the side effect associated with a satisfied rule.
+
+```java
+public interface Rule {
+
+ String name();
+
+ boolean evaluate(T context);
+
+ void execute(T context);
+}
+```
+
+The context is an immutable value object that carries the data the rules inspect.
+
+```java
+public record LoanApplication(
+ int age, double monthlyIncome, double loanAmount, int creditScore) {}
+```
+
+Concrete rules encapsulate one criterion each. New criteria are added by writing new classes — the engine never changes.
+
+```java
+public class MinimumAgeRule implements Rule {
+
+ private final int minimumAge;
+
+ public MinimumAgeRule(int minimumAge) {
+ this.minimumAge = minimumAge;
+ }
+
+ @Override
+ public String name() {
+ return "MinimumAgeRule";
+ }
+
+ @Override
+ public boolean evaluate(LoanApplication context) {
+ return context.age() >= minimumAge;
+ }
+
+ @Override
+ public void execute(LoanApplication context) {
+ LOGGER.info("Applicant age {} meets the minimum age of {}.", context.age(), minimumAge);
+ }
+}
+```
+
+The engine holds a defensively copied, immutable list of rules. It evaluates every rule in insertion order, executes the ones that pass, and reports the combined outcome. It never stops at the first failure, so a single run reports *all* the reasons an application was rejected.
+
+```java
+public class RuleEngine {
+
+ private final List> rules;
+
+ public RuleEngine(List> rules) {
+ this.rules = List.copyOf(rules);
+ }
+
+ public RuleEngineResult run(T context) {
+ Objects.requireNonNull(context, "context must not be null");
+ List passed = new ArrayList<>();
+ List failed = new ArrayList<>();
+ for (Rule rule : rules) {
+ if (rule.evaluate(context)) {
+ rule.execute(context);
+ passed.add(rule.name());
+ } else {
+ failed.add(rule.name());
+ }
+ }
+ return new RuleEngineResult(failed.isEmpty(), passed, failed);
+ }
+}
+```
+
+The result is an immutable value object describing the run.
+
+```java
+public record RuleEngineResult(
+ boolean approved, List passedRules, List failedRules) {
+
+ public RuleEngineResult {
+ passedRules = List.copyOf(passedRules);
+ failedRules = List.copyOf(failedRules);
+ }
+}
+```
+
+Putting it together, the `App` builds an engine and runs two applications through it.
+
+```java
+var engine =
+ new RuleEngine<>(
+ List.of(
+ new MinimumAgeRule(18),
+ new MinimumIncomeRule(2000.0),
+ new CreditScoreRule(650)));
+
+engine.run(new LoanApplication(30, 3500.0, 15000.0, 720)); // approved
+engine.run(new LoanApplication(17, 1500.0, 15000.0, 720)); // rejected: age and income
+```
+
+Program output:
+
+```
+Applicant age 30 meets the minimum age of 18.
+Applicant income 3500.0 meets the minimum income of 2000.0.
+Applicant credit score 720 meets the minimum score of 650.
+Loan approved. Passed rules: [MinimumAgeRule, MinimumIncomeRule, CreditScoreRule]
+Applicant credit score 720 meets the minimum score of 650.
+Loan rejected. Failed rules: [MinimumAgeRule, MinimumIncomeRule]
+```
+
+### Behavioral decisions
+
+The example commits to the following semantics, each covered by tests:
+
+* `evaluate` returning `true` means the rule **passes** (its condition is satisfied); a passing rule then has its `execute` action run.
+* The engine evaluates **every** rule; it does not stop after the first failure, so all rejection reasons are reported.
+* Outcomes are reported as an immutable `RuleEngineResult` carrying the overall `approved` flag plus the names of the passed and failed rules.
+* Rules execute in **insertion order**.
+* A `null` context passed to `run` throws `NullPointerException`; a `null` rule collection or a `null` rule element is rejected on construction.
+* An engine with **no rules** approves vacuously (nothing failed).
+
+## Class diagram
+
+See [rule-engine.urm.puml](./etc/rule-engine.urm.puml) for the PlantUML class diagram.
+
+## When to Use the Rule Engine Pattern in Java
+
+* Decision logic is a growing set of independent conditions that would otherwise become a large, hard-to-read nested `if/else`.
+* Rules must be added, removed, or reordered without touching the code that coordinates them.
+* You need to report *all* failing conditions, not just the first one.
+* Business rules deserve to be unit tested in isolation.
+
+## Real-World Applications of Rule Engine Pattern in Java
+
+* Loan, insurance, and credit approval workflows.
+* Validation frameworks such as Bean Validation, where each constraint is an independent rule.
+* Fraud detection and risk scoring, where many independent checks contribute to one decision.
+* Pricing, discount, and promotion eligibility engines.
+
+## Benefits and Trade-offs of Rule Engine Pattern
+
+Benefits
+
+* Encapsulation: each rule owns one criterion.
+* Extensibility: new rules are new classes; the engine is closed for modification.
+* Testability: rules and the engine can be tested independently.
+* Transparency: the result lists every passed and failed rule.
+
+Trade-offs
+
+* Debugging a decision means tracing several small objects instead of reading one block.
+* Rule ordering and conflicts must be managed deliberately when rules are not independent.
+* Over-abstracting trivial logic into a rule engine adds indirection that a simple `if` would not.
+
+## Related Java Design Patterns
+
+* [Specification](../specification): combines boolean criteria that an object must satisfy; a Rule Engine orchestrates and executes such criteria.
+* [Chain of Responsibility](../chain-of-responsibility): passes a request along handlers; a Rule Engine instead runs every rule against one context.
+* [Strategy](../strategy): each rule is effectively a pluggable strategy for one decision.
+* [Command](../command): a rule's `execute` action resembles an encapsulated command.
+
+## References and Credits
+
+* [Patterns of Enterprise Application Architecture](https://www.amazon.com/gp/product/0321127420) (Martin Fowler)
+* [Should I use a Rules Engine? (Martin Fowler)](https://martinfowler.com/bliki/RulesEngine.html)
diff --git a/rule-engine/etc/rule-engine.urm.puml b/rule-engine/etc/rule-engine.urm.puml
new file mode 100644
index 000000000000..c560f7862485
--- /dev/null
+++ b/rule-engine/etc/rule-engine.urm.puml
@@ -0,0 +1,61 @@
+@startuml
+package com.iluwatar.ruleengine {
+ interface Rule {
+ + name() : String {abstract}
+ + evaluate(context : T) : boolean {abstract}
+ + execute(context : T) : void {abstract}
+ }
+ class RuleEngine {
+ - rules : List>
+ + RuleEngine(rules : List>)
+ + run(context : T) : RuleEngineResult
+ + rules() : List>
+ }
+ class RuleEngineResult {
+ + RuleEngineResult(approved : boolean, passedRules : List, failedRules : List)
+ + approved() : boolean
+ + passedRules() : List
+ + failedRules() : List
+ }
+ class LoanApplication {
+ + LoanApplication(age : int, monthlyIncome : double, loanAmount : double, creditScore : int)
+ + age() : int
+ + monthlyIncome() : double
+ + loanAmount() : double
+ + creditScore() : int
+ }
+ class MinimumAgeRule {
+ - minimumAge : int
+ + MinimumAgeRule(minimumAge : int)
+ + name() : String
+ + evaluate(context : LoanApplication) : boolean
+ + execute(context : LoanApplication) : void
+ }
+ class MinimumIncomeRule {
+ - minimumIncome : double
+ + MinimumIncomeRule(minimumIncome : double)
+ + name() : String
+ + evaluate(context : LoanApplication) : boolean
+ + execute(context : LoanApplication) : void
+ }
+ class CreditScoreRule {
+ - minimumScore : int
+ + CreditScoreRule(minimumScore : int)
+ + name() : String
+ + evaluate(context : LoanApplication) : boolean
+ + execute(context : LoanApplication) : void
+ }
+ class App {
+ + App()
+ + main(args : String[]) : void
+ }
+}
+RuleEngine --> "*" Rule
+RuleEngine ..> RuleEngineResult
+MinimumAgeRule ..|> Rule
+MinimumIncomeRule ..|> Rule
+CreditScoreRule ..|> Rule
+MinimumAgeRule ..> LoanApplication
+MinimumIncomeRule ..> LoanApplication
+CreditScoreRule ..> LoanApplication
+@enduml
diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml
new file mode 100644
index 000000000000..29738d783a99
--- /dev/null
+++ b/rule-engine/pom.xml
@@ -0,0 +1,70 @@
+
+
+
+
+ java-design-patterns
+ com.iluwatar
+ 1.26.0-SNAPSHOT
+
+ 4.0.0
+ rule-engine
+
+
+ org.slf4j
+ slf4j-api
+
+
+ ch.qos.logback
+ logback-classic
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+
+
+
+
+
+ com.iluwatar.ruleengine.App
+
+
+
+
+
+
+
+
+
diff --git a/rule-engine/src/main/java/com/iluwatar/ruleengine/App.java b/rule-engine/src/main/java/com/iluwatar/ruleengine/App.java
new file mode 100644
index 000000000000..78a51f152b84
--- /dev/null
+++ b/rule-engine/src/main/java/com/iluwatar/ruleengine/App.java
@@ -0,0 +1,69 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.ruleengine;
+
+import java.util.List;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * The Rule Engine pattern encapsulates each business rule as an independent object and lets a
+ * {@link RuleEngine} evaluate a collection of them against a shared context. This replaces large,
+ * tangled conditional blocks with small, testable rules that can be added or removed without
+ * touching the orchestration logic.
+ *
+ * This demo builds a loan approval engine from three rules and runs two applications through it:
+ * one that satisfies every rule and one that fails two of them. Because the engine evaluates every
+ * rule rather than stopping at the first failure, the result reports all the reasons an application
+ * was rejected.
+ */
+@Slf4j
+public class App {
+
+ /**
+ * Program entry point.
+ *
+ * @param args command line arguments
+ */
+ public static void main(String[] args) {
+ var engine =
+ new RuleEngine<>(
+ List.of(
+ new MinimumAgeRule(18), new MinimumIncomeRule(2000.0), new CreditScoreRule(650)));
+
+ var approved = new LoanApplication(30, 3500.0, 15000.0, 720);
+ var rejected = new LoanApplication(17, 1500.0, 15000.0, 720);
+
+ report(engine.run(approved));
+ report(engine.run(rejected));
+ }
+
+ private static void report(RuleEngineResult result) {
+ if (result.approved()) {
+ LOGGER.info("Loan approved. Passed rules: {}", result.passedRules());
+ } else {
+ LOGGER.info("Loan rejected. Failed rules: {}", result.failedRules());
+ }
+ }
+}
diff --git a/rule-engine/src/main/java/com/iluwatar/ruleengine/CreditScoreRule.java b/rule-engine/src/main/java/com/iluwatar/ruleengine/CreditScoreRule.java
new file mode 100644
index 000000000000..28cd59baadbb
--- /dev/null
+++ b/rule-engine/src/main/java/com/iluwatar/ruleengine/CreditScoreRule.java
@@ -0,0 +1,61 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.ruleengine;
+
+import lombok.extern.slf4j.Slf4j;
+
+/** Passes when the applicant's credit score reaches the required minimum. */
+@Slf4j
+public class CreditScoreRule implements Rule {
+
+ private final int minimumScore;
+
+ /**
+ * Creates the rule.
+ *
+ * @param minimumScore the inclusive minimum credit score the applicant must have
+ */
+ public CreditScoreRule(int minimumScore) {
+ this.minimumScore = minimumScore;
+ }
+
+ @Override
+ public String name() {
+ return "CreditScoreRule";
+ }
+
+ @Override
+ public boolean evaluate(LoanApplication context) {
+ return context.creditScore() >= minimumScore;
+ }
+
+ @Override
+ public void execute(LoanApplication context) {
+ LOGGER.info(
+ "Applicant credit score {} meets the minimum score of {}.",
+ context.creditScore(),
+ minimumScore);
+ }
+}
diff --git a/rule-engine/src/main/java/com/iluwatar/ruleengine/LoanApplication.java b/rule-engine/src/main/java/com/iluwatar/ruleengine/LoanApplication.java
new file mode 100644
index 000000000000..0eda1f17d3e2
--- /dev/null
+++ b/rule-engine/src/main/java/com/iluwatar/ruleengine/LoanApplication.java
@@ -0,0 +1,38 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.ruleengine;
+
+/**
+ * Immutable context that the loan {@link Rule rules} are evaluated against.
+ *
+ * Using a record keeps the domain object immutable and free of behaviour, so the business logic
+ * lives entirely inside the rules rather than being scattered across the data it operates on.
+ *
+ * @param age applicant age in years
+ * @param monthlyIncome applicant net monthly income
+ * @param loanAmount requested loan amount
+ * @param creditScore applicant credit score
+ */
+public record LoanApplication(int age, double monthlyIncome, double loanAmount, int creditScore) {}
diff --git a/rule-engine/src/main/java/com/iluwatar/ruleengine/MinimumAgeRule.java b/rule-engine/src/main/java/com/iluwatar/ruleengine/MinimumAgeRule.java
new file mode 100644
index 000000000000..71532dde4707
--- /dev/null
+++ b/rule-engine/src/main/java/com/iluwatar/ruleengine/MinimumAgeRule.java
@@ -0,0 +1,58 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.ruleengine;
+
+import lombok.extern.slf4j.Slf4j;
+
+/** Passes when the applicant is at least the required minimum age. */
+@Slf4j
+public class MinimumAgeRule implements Rule {
+
+ private final int minimumAge;
+
+ /**
+ * Creates the rule.
+ *
+ * @param minimumAge the inclusive minimum age the applicant must reach
+ */
+ public MinimumAgeRule(int minimumAge) {
+ this.minimumAge = minimumAge;
+ }
+
+ @Override
+ public String name() {
+ return "MinimumAgeRule";
+ }
+
+ @Override
+ public boolean evaluate(LoanApplication context) {
+ return context.age() >= minimumAge;
+ }
+
+ @Override
+ public void execute(LoanApplication context) {
+ LOGGER.info("Applicant age {} meets the minimum age of {}.", context.age(), minimumAge);
+ }
+}
diff --git a/rule-engine/src/main/java/com/iluwatar/ruleengine/MinimumIncomeRule.java b/rule-engine/src/main/java/com/iluwatar/ruleengine/MinimumIncomeRule.java
new file mode 100644
index 000000000000..fc6d40964fc9
--- /dev/null
+++ b/rule-engine/src/main/java/com/iluwatar/ruleengine/MinimumIncomeRule.java
@@ -0,0 +1,61 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.ruleengine;
+
+import lombok.extern.slf4j.Slf4j;
+
+/** Passes when the applicant's monthly income reaches the required minimum. */
+@Slf4j
+public class MinimumIncomeRule implements Rule {
+
+ private final double minimumIncome;
+
+ /**
+ * Creates the rule.
+ *
+ * @param minimumIncome the inclusive minimum monthly income the applicant must earn
+ */
+ public MinimumIncomeRule(double minimumIncome) {
+ this.minimumIncome = minimumIncome;
+ }
+
+ @Override
+ public String name() {
+ return "MinimumIncomeRule";
+ }
+
+ @Override
+ public boolean evaluate(LoanApplication context) {
+ return context.monthlyIncome() >= minimumIncome;
+ }
+
+ @Override
+ public void execute(LoanApplication context) {
+ LOGGER.info(
+ "Applicant income {} meets the minimum income of {}.",
+ context.monthlyIncome(),
+ minimumIncome);
+ }
+}
diff --git a/rule-engine/src/main/java/com/iluwatar/ruleengine/Rule.java b/rule-engine/src/main/java/com/iluwatar/ruleengine/Rule.java
new file mode 100644
index 000000000000..723b31e73b52
--- /dev/null
+++ b/rule-engine/src/main/java/com/iluwatar/ruleengine/Rule.java
@@ -0,0 +1,61 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.ruleengine;
+
+/**
+ * Abstraction for a single, self-contained business rule.
+ *
+ * A rule separates the decision ({@link #evaluate(Object)}) from the action ({@link
+ * #execute(Object)}). {@code evaluate} inspects the context and reports whether the rule's
+ * condition is satisfied, while {@code execute} performs the side effect associated with a
+ * satisfied rule. Keeping the two responsibilities apart lets a {@link RuleEngine} decide which
+ * rules apply before running any action, and lets new rules be added without touching the engine.
+ *
+ * @param type of the context the rule is evaluated against
+ */
+public interface Rule {
+
+ /**
+ * Returns a human readable identifier used when reporting rule outcomes.
+ *
+ * @return the rule name
+ */
+ String name();
+
+ /**
+ * Evaluates the rule against the given context.
+ *
+ * @param context the immutable input the rule inspects
+ * @return {@code true} when the rule's condition is satisfied, {@code false} otherwise
+ */
+ boolean evaluate(T context);
+
+ /**
+ * Performs the action associated with a satisfied rule.
+ *
+ * @param context the immutable input the action operates on
+ */
+ void execute(T context);
+}
diff --git a/rule-engine/src/main/java/com/iluwatar/ruleengine/RuleEngine.java b/rule-engine/src/main/java/com/iluwatar/ruleengine/RuleEngine.java
new file mode 100644
index 000000000000..85a8db9d74b5
--- /dev/null
+++ b/rule-engine/src/main/java/com/iluwatar/ruleengine/RuleEngine.java
@@ -0,0 +1,91 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.ruleengine;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Orchestrates a fixed collection of {@link Rule rules} against a context.
+ *
+ * The engine owns the coordination logic that would otherwise live in a large nested {@code
+ * if}/{@code else} block: it evaluates every rule in insertion order, runs the action of each rule
+ * whose condition is satisfied, and reports the combined outcome as a {@link RuleEngineResult}.
+ * Evaluation never stops early, so a single run reports all failing rules rather than only
+ * the first, which is what makes rejection reasons useful.
+ *
+ *
The rule collection is copied defensively on construction, making the engine immutable and
+ * safe to reuse across many contexts. New behaviour is added by supplying additional {@link Rule}
+ * implementations; the engine itself never changes.
+ *
+ * @param type of the context the rules are evaluated against
+ */
+public class RuleEngine {
+
+ private final List> rules;
+
+ /**
+ * Creates an engine for the given rules.
+ *
+ * @param rules the rules to evaluate, applied in iteration order; must not be {@code null} or
+ * contain {@code null} elements
+ * @throws NullPointerException if {@code rules} is {@code null} or contains a {@code null} rule
+ */
+ public RuleEngine(List> rules) {
+ this.rules = List.copyOf(rules);
+ }
+
+ /**
+ * Evaluates every rule against the context and executes the rules that pass.
+ *
+ * @param context the context inspected by the rules; must not be {@code null}
+ * @return the combined outcome of the run
+ * @throws NullPointerException if {@code context} is {@code null}
+ */
+ public RuleEngineResult run(T context) {
+ Objects.requireNonNull(context, "context must not be null");
+ List passed = new ArrayList<>();
+ List failed = new ArrayList<>();
+ for (Rule rule : rules) {
+ if (rule.evaluate(context)) {
+ rule.execute(context);
+ passed.add(rule.name());
+ } else {
+ failed.add(rule.name());
+ }
+ }
+ return new RuleEngineResult(failed.isEmpty(), passed, failed);
+ }
+
+ /**
+ * Returns the rules held by this engine.
+ *
+ * @return an immutable view of the configured rules
+ */
+ public List> rules() {
+ return rules;
+ }
+}
diff --git a/rule-engine/src/main/java/com/iluwatar/ruleengine/RuleEngineResult.java b/rule-engine/src/main/java/com/iluwatar/ruleengine/RuleEngineResult.java
new file mode 100644
index 000000000000..868e3986602f
--- /dev/null
+++ b/rule-engine/src/main/java/com/iluwatar/ruleengine/RuleEngineResult.java
@@ -0,0 +1,48 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.ruleengine;
+
+import java.util.List;
+
+/**
+ * Immutable outcome produced by {@link RuleEngine#run(Object)}.
+ *
+ * The result reports whether every rule passed and, for transparency, the names of the rules
+ * that passed and failed in the order they were evaluated. The lists are copied on construction so
+ * the result cannot be mutated by callers or by later changes to the source collections.
+ *
+ * @param approved {@code true} when no rule failed
+ * @param passedRules names of the rules whose condition was satisfied, in evaluation order
+ * @param failedRules names of the rules whose condition was not satisfied, in evaluation order
+ */
+public record RuleEngineResult(
+ boolean approved, List passedRules, List failedRules) {
+
+ /** Defensive copy keeps the value object immutable. */
+ public RuleEngineResult {
+ passedRules = List.copyOf(passedRules);
+ failedRules = List.copyOf(failedRules);
+ }
+}
diff --git a/rule-engine/src/test/java/com/iluwatar/ruleengine/AppTest.java b/rule-engine/src/test/java/com/iluwatar/ruleengine/AppTest.java
new file mode 100644
index 000000000000..a8746fa0c387
--- /dev/null
+++ b/rule-engine/src/test/java/com/iluwatar/ruleengine/AppTest.java
@@ -0,0 +1,37 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.ruleengine;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+import org.junit.jupiter.api.Test;
+
+class AppTest {
+
+ @Test
+ void shouldLaunchApp() {
+ assertDoesNotThrow(() -> App.main(new String[] {}));
+ }
+}
diff --git a/rule-engine/src/test/java/com/iluwatar/ruleengine/LoanRulesTest.java b/rule-engine/src/test/java/com/iluwatar/ruleengine/LoanRulesTest.java
new file mode 100644
index 000000000000..80ba160b1ffa
--- /dev/null
+++ b/rule-engine/src/test/java/com/iluwatar/ruleengine/LoanRulesTest.java
@@ -0,0 +1,81 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.ruleengine;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+/** Verifies each concrete rule independently at, above and below its threshold. */
+class LoanRulesTest {
+
+ private static LoanApplication applicant(int age, double income, int score) {
+ return new LoanApplication(age, income, 10000.0, score);
+ }
+
+ @Test
+ void minimumAgeRulePassesAtOrAboveThreshold() {
+ var rule = new MinimumAgeRule(18);
+ assertTrue(rule.evaluate(applicant(18, 3000.0, 700)));
+ assertTrue(rule.evaluate(applicant(40, 3000.0, 700)));
+ assertDoesNotThrow(() -> rule.execute(applicant(18, 3000.0, 700)));
+ }
+
+ @Test
+ void minimumAgeRuleFailsBelowThreshold() {
+ var rule = new MinimumAgeRule(18);
+ assertFalse(rule.evaluate(applicant(17, 3000.0, 700)));
+ }
+
+ @Test
+ void minimumIncomeRulePassesAtOrAboveThreshold() {
+ var rule = new MinimumIncomeRule(2000.0);
+ assertTrue(rule.evaluate(applicant(30, 2000.0, 700)));
+ assertTrue(rule.evaluate(applicant(30, 5000.0, 700)));
+ assertDoesNotThrow(() -> rule.execute(applicant(30, 2000.0, 700)));
+ }
+
+ @Test
+ void minimumIncomeRuleFailsBelowThreshold() {
+ var rule = new MinimumIncomeRule(2000.0);
+ assertFalse(rule.evaluate(applicant(30, 1999.99, 700)));
+ }
+
+ @Test
+ void creditScoreRulePassesAtOrAboveThreshold() {
+ var rule = new CreditScoreRule(650);
+ assertTrue(rule.evaluate(applicant(30, 3000.0, 650)));
+ assertTrue(rule.evaluate(applicant(30, 3000.0, 800)));
+ assertDoesNotThrow(() -> rule.execute(applicant(30, 3000.0, 650)));
+ }
+
+ @Test
+ void creditScoreRuleFailsBelowThreshold() {
+ var rule = new CreditScoreRule(650);
+ assertFalse(rule.evaluate(applicant(30, 3000.0, 649)));
+ }
+}
diff --git a/rule-engine/src/test/java/com/iluwatar/ruleengine/RuleEngineResultTest.java b/rule-engine/src/test/java/com/iluwatar/ruleengine/RuleEngineResultTest.java
new file mode 100644
index 000000000000..0bae5dc12c0d
--- /dev/null
+++ b/rule-engine/src/test/java/com/iluwatar/ruleengine/RuleEngineResultTest.java
@@ -0,0 +1,55 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.ruleengine;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+class RuleEngineResultTest {
+
+ @Test
+ void copiesListsDefensivelyOnConstruction() {
+ var passed = new ArrayList<>(List.of("MinimumAgeRule"));
+ var failed = new ArrayList<>(List.of("CreditScoreRule"));
+ var result = new RuleEngineResult(false, passed, failed);
+
+ passed.add("MinimumIncomeRule"); // mutate the sources after construction
+ failed.clear();
+
+ assertEquals(List.of("MinimumAgeRule"), result.passedRules());
+ assertEquals(List.of("CreditScoreRule"), result.failedRules());
+ }
+
+ @Test
+ void exposedListsAreUnmodifiable() {
+ var result = new RuleEngineResult(true, List.of("MinimumAgeRule"), List.of());
+ assertThrows(UnsupportedOperationException.class, () -> result.passedRules().add("x"));
+ assertThrows(UnsupportedOperationException.class, () -> result.failedRules().add("x"));
+ }
+}
diff --git a/rule-engine/src/test/java/com/iluwatar/ruleengine/RuleEngineTest.java b/rule-engine/src/test/java/com/iluwatar/ruleengine/RuleEngineTest.java
new file mode 100644
index 000000000000..fd8f89943ac9
--- /dev/null
+++ b/rule-engine/src/test/java/com/iluwatar/ruleengine/RuleEngineTest.java
@@ -0,0 +1,122 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.ruleengine;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+class RuleEngineTest {
+
+ private static RuleEngine loanEngine() {
+ return new RuleEngine<>(
+ List.of(new MinimumAgeRule(18), new MinimumIncomeRule(2000.0), new CreditScoreRule(650)));
+ }
+
+ @Test
+ void approvesWhenEveryRulePasses() {
+ var result = loanEngine().run(new LoanApplication(30, 3000.0, 10000.0, 700));
+ assertTrue(result.approved());
+ assertEquals(
+ List.of("MinimumAgeRule", "MinimumIncomeRule", "CreditScoreRule"), result.passedRules());
+ assertTrue(result.failedRules().isEmpty());
+ }
+
+ @Test
+ void rejectsWhenASingleRuleFails() {
+ var result = loanEngine().run(new LoanApplication(30, 3000.0, 10000.0, 600));
+ assertFalse(result.approved());
+ assertEquals(List.of("CreditScoreRule"), result.failedRules());
+ assertEquals(List.of("MinimumAgeRule", "MinimumIncomeRule"), result.passedRules());
+ }
+
+ @Test
+ void reportsEveryFailingRuleWithoutStoppingEarly() {
+ var result = loanEngine().run(new LoanApplication(16, 500.0, 10000.0, 400));
+ assertFalse(result.approved());
+ assertEquals(
+ List.of("MinimumAgeRule", "MinimumIncomeRule", "CreditScoreRule"), result.failedRules());
+ assertTrue(result.passedRules().isEmpty());
+ }
+
+ @Test
+ void evaluatesRulesInInsertionOrder() {
+ var engine =
+ new RuleEngine<>(
+ List.of(
+ new CreditScoreRule(650), new MinimumAgeRule(18), new MinimumIncomeRule(2000.0)));
+ var result = engine.run(new LoanApplication(16, 500.0, 10000.0, 400));
+ assertEquals(
+ List.of("CreditScoreRule", "MinimumAgeRule", "MinimumIncomeRule"), result.failedRules());
+ }
+
+ @Test
+ void emptyEngineApprovesVacuously() {
+ var result =
+ new RuleEngine(List.of()).run(new LoanApplication(1, 1.0, 1.0, 1));
+ assertTrue(result.approved());
+ assertTrue(result.passedRules().isEmpty());
+ assertTrue(result.failedRules().isEmpty());
+ }
+
+ @Test
+ void rejectsNullContext() {
+ assertThrows(NullPointerException.class, () -> loanEngine().run(null));
+ }
+
+ @Test
+ void rejectsNullRuleCollection() {
+ assertThrows(NullPointerException.class, () -> new RuleEngine(null));
+ }
+
+ @Test
+ void rejectsNullRuleElement() {
+ var rules = new ArrayList>();
+ rules.add(null);
+ assertThrows(NullPointerException.class, () -> new RuleEngine<>(rules));
+ }
+
+ @Test
+ void copiesRulesDefensivelyOnConstruction() {
+ var rules = new ArrayList>();
+ rules.add(new MinimumAgeRule(18));
+ var engine = new RuleEngine<>(rules);
+
+ rules.add(new CreditScoreRule(650)); // mutate the source after construction
+ assertEquals(1, engine.rules().size());
+ }
+
+ @Test
+ void exposedRulesAreUnmodifiable() {
+ var engine = loanEngine();
+ assertThrows(
+ UnsupportedOperationException.class, () -> engine.rules().add(new MinimumAgeRule(21)));
+ }
+}
diff --git a/subclass-sandbox/pom.xml b/subclass-sandbox/pom.xml
index 1085dddb087b..79aa0b63cbfd 100644
--- a/subclass-sandbox/pom.xml
+++ b/subclass-sandbox/pom.xml
@@ -35,7 +35,7 @@
subclass-sandbox
2.0.18
- 1.5.34
+ 1.5.35