Security is a cross-cutting concern that the {@link ServiceBus} enforces centrally, so the
+ * services themselves stay free of security code. The model here is deliberately minimal: a set of
+ * valid credentials and a set of protected service names. In a real SOA this is where the bus would
+ * authenticate the caller and authorise the operation against a security token service.
+ */
+public class AccessPolicy {
+
+ private final Set The building blocks demonstrated here are:
+ *
+ * The demo registers the services behind a bus that protects the payment service, places an
+ * order with a valid credential that succeeds, an order that fails because of insufficient stock,
+ * addresses a service that does not exist, and finally places an order anonymously, which the bus
+ * rejects when the order service tries to charge the customer.
+ */
+@Slf4j
+public class App {
+
+ private static final String CHECKOUT_CREDENTIAL = "checkout-service-key";
+
+ /**
+ * Program entry point.
+ *
+ * @param args command line arguments, not used
+ */
+ public static void main(String[] args) {
+ LOGGER.info("Bootstrapping the service registry and the service bus");
+ var registry = new ServiceRegistry();
+ var policy = new AccessPolicy(Set.of(CHECKOUT_CREDENTIAL), Set.of(PaymentService.NAME));
+ var bus = new ServiceBus(registry, policy);
+
+ registry.register(new CustomerService());
+ registry.register(new InventoryService());
+ registry.register(new PaymentService());
+ registry.register(new OrderService(bus));
+ LOGGER.info("Available services: {}", registry.serviceNames());
+
+ LOGGER.info("Placing an order with a valid credential, it should succeed");
+ var accepted =
+ bus.send(
+ new ServiceRequest(
+ OrderService.NAME,
+ "placeOrder",
+ Map.of("customerId", "C-1", "sku", "LAPTOP", "quantity", 2, "amount", 899.0),
+ CHECKOUT_CREDENTIAL));
+ LOGGER.info("Order outcome: {}", accepted);
+
+ LOGGER.info("Placing an order that should be rejected because of stock");
+ var rejected =
+ bus.send(
+ new ServiceRequest(
+ OrderService.NAME,
+ "placeOrder",
+ Map.of("customerId", "C-2", "sku", "PHONE", "quantity", 50, "amount", 499.0),
+ CHECKOUT_CREDENTIAL));
+ LOGGER.info("Order outcome: {}", rejected);
+
+ LOGGER.info("Addressing a service that is not registered");
+ var unknown = bus.send(new ServiceRequest("shipping", "ship", Map.of("orderId", "ORD-1")));
+ LOGGER.info("Bus outcome: {}", unknown);
+
+ LOGGER.info("Placing an order anonymously, the bus should deny access to payment");
+ var denied =
+ bus.send(
+ new ServiceRequest(
+ OrderService.NAME,
+ "placeOrder",
+ Map.of("customerId", "C-1", "sku", "LAPTOP", "quantity", 1, "amount", 899.0)));
+ LOGGER.info("Order outcome: {}", denied);
+ }
+}
diff --git a/service-oriented-architecture/src/main/java/com/iluwatar/soa/Customer.java b/service-oriented-architecture/src/main/java/com/iluwatar/soa/Customer.java
new file mode 100644
index 000000000000..de9c12e31c31
--- /dev/null
+++ b/service-oriented-architecture/src/main/java/com/iluwatar/soa/Customer.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.soa;
+
+/**
+ * A customer as exposed by the {@link CustomerService}.
+ *
+ * @param id the customer identifier
+ * @param name the display name
+ * @param email the contact address
+ */
+public record Customer(String id, String name, String email) {}
diff --git a/service-oriented-architecture/src/main/java/com/iluwatar/soa/CustomerService.java b/service-oriented-architecture/src/main/java/com/iluwatar/soa/CustomerService.java
new file mode 100644
index 000000000000..24cf9dede394
--- /dev/null
+++ b/service-oriented-architecture/src/main/java/com/iluwatar/soa/CustomerService.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.soa;
+
+import java.util.Map;
+
+/**
+ * Enterprise service that owns customer data.
+ *
+ * Operations:
+ *
+ * Operations:
+ *
+ * The order service depends only on service names and message contracts, never on the classes
+ * that provide them. It sends messages through the {@link ServiceBus}, forwards the caller's
+ * credential so that the bus can authorise each downstream call, and composes the responses. This
+ * is how SOA builds higher level services out of reusable lower level ones.
+ *
+ * Operations:
+ *
+ * Operations:
+ *
+ * In a service-oriented architecture a service is a coarse-grained, stateless unit of business
+ * functionality. Consumers never depend on the implementation class; they only know the service
+ * name and the message formats ({@link ServiceRequest} and {@link ServiceResponse}). This keeps
+ * services loosely coupled and independently replaceable.
+ */
+public interface Service {
+
+ /** The unique name consumers use to address this service through the bus. */
+ String name();
+
+ /**
+ * Handles a single request. Implementations must be stateless between calls: every request
+ * carries everything the service needs to process it.
+ *
+ * @param request the incoming message
+ * @return the outcome of the operation
+ */
+ ServiceResponse handle(ServiceRequest request);
+}
diff --git a/service-oriented-architecture/src/main/java/com/iluwatar/soa/ServiceBus.java b/service-oriented-architecture/src/main/java/com/iluwatar/soa/ServiceBus.java
new file mode 100644
index 000000000000..fc6615d0f7d7
--- /dev/null
+++ b/service-oriented-architecture/src/main/java/com/iluwatar/soa/ServiceBus.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.soa;
+
+import java.util.concurrent.TimeUnit;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * The service bus is the communication backbone of the architecture.
+ *
+ * Consumers hand every request to the bus. The bus discovers the target service in the {@link
+ * ServiceRegistry}, applies cross-cutting concerns in one place (access control through the {@link
+ * AccessPolicy}, tracing and timing) and shields consumers from provider failures by translating
+ * exceptions into error responses. Because the consumer only talks to the bus, the provider can be
+ * replaced or relocated transparently.
+ */
+@Slf4j
+public class ServiceBus {
+
+ private final ServiceRegistry registry;
+ private final AccessPolicy policy;
+
+ /** Creates a bus that routes to the given registry and lets every request through. */
+ public ServiceBus(ServiceRegistry registry) {
+ this(registry, AccessPolicy.permitAll());
+ }
+
+ /** Creates a bus that routes to the given registry and enforces the given access policy. */
+ public ServiceBus(ServiceRegistry registry, AccessPolicy policy) {
+ this.registry = registry;
+ this.policy = policy;
+ }
+
+ /**
+ * Routes the request to the service it addresses.
+ *
+ * @param request the message to deliver
+ * @return the service response, or an error response when the service is unknown, access is
+ * denied or the service fails
+ */
+ public ServiceResponse send(ServiceRequest request) {
+ var service = registry.lookup(request.service());
+ if (service.isEmpty()) {
+ LOGGER.warn("No service registered under '{}'", request.service());
+ return ServiceResponse.error("No such service: " + request.service());
+ }
+ if (!policy.allows(request)) {
+ LOGGER.warn(
+ "Access denied to {}.{} for {} caller",
+ request.service(),
+ request.operation(),
+ request.credential() == null ? "anonymous" : "credentialed");
+ return ServiceResponse.error("Access denied to " + request.service());
+ }
+ LOGGER.info("-> {}.{} payload={}", request.service(), request.operation(), request.payload());
+ var start = System.nanoTime();
+ try {
+ var response = service.get().handle(request);
+ LOGGER.info(
+ "<- {}.{} success={} in {} ms",
+ request.service(),
+ request.operation(),
+ response.success(),
+ TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start));
+ return response;
+ } catch (RuntimeException e) {
+ LOGGER.error("<- {}.{} failed: {}", request.service(), request.operation(), e.getMessage());
+ return ServiceResponse.error("Service " + request.service() + " failed: " + e.getMessage());
+ }
+ }
+}
diff --git a/service-oriented-architecture/src/main/java/com/iluwatar/soa/ServiceRegistry.java b/service-oriented-architecture/src/main/java/com/iluwatar/soa/ServiceRegistry.java
new file mode 100644
index 000000000000..883e32f49242
--- /dev/null
+++ b/service-oriented-architecture/src/main/java/com/iluwatar/soa/ServiceRegistry.java
@@ -0,0 +1,68 @@
+/*
+ * 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.soa;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * The service registry provides discovery: services publish themselves under a name and consumers
+ * locate them at runtime instead of hard-wiring implementations.
+ *
+ * The registry is the only place that knows which concrete class provides a contract, so a
+ * service can be swapped or moved without touching its consumers.
+ */
+@Slf4j
+public class ServiceRegistry {
+
+ private final Map The request names the target service and the operation to invoke, carries a flat, immutable
+ * payload and optionally the caller's credential. Because the payload is plain data rather than
+ * typed Java objects, the same contract could be transported as SOAP, JSON or any other
+ * interoperable format.
+ *
+ * @param service the name of the target service
+ * @param operation the operation the target service should perform
+ * @param payload the parameters of the operation
+ * @param credential the caller's credential, {@code null} for an anonymous caller
+ */
+public record ServiceRequest(
+ String service, String operation, Map Every operation answers with the same envelope so that consumers and the bus can treat all
+ * services uniformly: a success flag, an optional body and a human readable message.
+ *
+ * @param success whether the operation completed
+ * @param body the result of a successful operation, may be {@code null}
+ * @param message a description of the failure, empty on success
+ */
+public record ServiceResponse(boolean success, Object body, String message) {
+
+ /** Creates a successful response carrying the given body. */
+ public static ServiceResponse ok(Object body) {
+ return new ServiceResponse(true, body, "");
+ }
+
+ /** Creates a failed response with the given explanation. */
+ public static ServiceResponse error(String message) {
+ return new ServiceResponse(false, null, message);
+ }
+}
diff --git a/service-oriented-architecture/src/test/java/com/iluwatar/soa/AccessPolicyTest.java b/service-oriented-architecture/src/test/java/com/iluwatar/soa/AccessPolicyTest.java
new file mode 100644
index 000000000000..22d0e809918a
--- /dev/null
+++ b/service-oriented-architecture/src/test/java/com/iluwatar/soa/AccessPolicyTest.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.soa;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Map;
+import java.util.Set;
+import org.junit.jupiter.api.Test;
+
+class AccessPolicyTest {
+
+ private final AccessPolicy policy = new AccessPolicy(Set.of("secret"), Set.of("payment"));
+
+ @Test
+ void permitAllShouldAllowEverything() {
+ var permitAll = AccessPolicy.permitAll();
+
+ assertTrue(permitAll.allows(request("payment", null)));
+ assertTrue(permitAll.allows(request("customer", null)));
+ }
+
+ @Test
+ void shouldDenyProtectedServiceWithoutCredential() {
+ assertFalse(policy.allows(request("payment", null)));
+ }
+
+ @Test
+ void shouldDenyProtectedServiceWithUnknownCredential() {
+ assertFalse(policy.allows(request("payment", "wrong")));
+ }
+
+ @Test
+ void shouldAllowProtectedServiceWithValidCredential() {
+ assertTrue(policy.allows(request("payment", "secret")));
+ }
+
+ @Test
+ void shouldAllowUnprotectedServiceAnonymously() {
+ assertTrue(policy.allows(request("customer", null)));
+ }
+
+ private static ServiceRequest request(String service, String credential) {
+ return new ServiceRequest(service, "op", Map.of(), credential);
+ }
+}
diff --git a/service-oriented-architecture/src/test/java/com/iluwatar/soa/AppTest.java b/service-oriented-architecture/src/test/java/com/iluwatar/soa/AppTest.java
new file mode 100644
index 000000000000..b2105dfe02b4
--- /dev/null
+++ b/service-oriented-architecture/src/test/java/com/iluwatar/soa/AppTest.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.soa;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.junit.jupiter.api.Test;
+
+class AppTest {
+
+ @Test
+ void shouldLaunchApp() {
+ assertDoesNotThrow(() -> App.main(new String[] {}));
+ }
+
+ @Test
+ void shouldBeInstantiable() {
+ assertNotNull(new App(), "App should be instantiable");
+ }
+}
diff --git a/service-oriented-architecture/src/test/java/com/iluwatar/soa/CustomerServiceTest.java b/service-oriented-architecture/src/test/java/com/iluwatar/soa/CustomerServiceTest.java
new file mode 100644
index 000000000000..ab12fed456f7
--- /dev/null
+++ b/service-oriented-architecture/src/test/java/com/iluwatar/soa/CustomerServiceTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.soa;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+class CustomerServiceTest {
+
+ private final CustomerService service = new CustomerService();
+
+ @Test
+ void shouldReturnKnownCustomer() {
+ var response =
+ service.handle(
+ new ServiceRequest(CustomerService.NAME, "getCustomer", Map.of("customerId", "C-2")));
+
+ assertTrue(response.success());
+ assertEquals(new Customer("C-2", "Bob Jones", "bob@example.com"), response.body());
+ }
+
+ @Test
+ void shouldFailForUnknownCustomer() {
+ var response =
+ service.handle(
+ new ServiceRequest(CustomerService.NAME, "getCustomer", Map.of("customerId", "C-9")));
+
+ assertFalse(response.success());
+ assertEquals("Unknown customer: C-9", response.message());
+ }
+
+ @Test
+ void shouldFailForUnknownOperation() {
+ var response = service.handle(new ServiceRequest(CustomerService.NAME, "delete", Map.of()));
+
+ assertFalse(response.success());
+ assertEquals("Unknown operation: delete", response.message());
+ }
+}
diff --git a/service-oriented-architecture/src/test/java/com/iluwatar/soa/InventoryServiceTest.java b/service-oriented-architecture/src/test/java/com/iluwatar/soa/InventoryServiceTest.java
new file mode 100644
index 000000000000..f852cf5261a4
--- /dev/null
+++ b/service-oriented-architecture/src/test/java/com/iluwatar/soa/InventoryServiceTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.soa;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+class InventoryServiceTest {
+
+ private final InventoryService service = new InventoryService(Map.of("LAPTOP", 3));
+
+ @Test
+ void shouldReportAvailability() {
+ assertEquals(true, check("LAPTOP", 3).body());
+ assertEquals(false, check("LAPTOP", 4).body());
+ assertEquals(false, check("TABLET", 1).body());
+ }
+
+ @Test
+ void shouldReserveAndReduceStock() {
+ var response = reserve("LAPTOP", 2);
+
+ assertTrue(response.success());
+ assertEquals(1, response.body());
+ assertEquals(false, check("LAPTOP", 2).body());
+ }
+
+ @Test
+ void shouldRejectReservationBeyondStock() {
+ var response = reserve("LAPTOP", 5);
+
+ assertFalse(response.success());
+ assertEquals("Insufficient stock for LAPTOP: requested 5, available 3", response.message());
+ assertEquals(true, check("LAPTOP", 3).body());
+ }
+
+ @Test
+ void shouldRejectReservationOfUnknownSku() {
+ var response = reserve("TABLET", 1);
+
+ assertFalse(response.success());
+ assertEquals("Insufficient stock for TABLET: requested 1, available 0", response.message());
+ }
+
+ @Test
+ void shouldFailForUnknownOperation() {
+ var response = service.handle(new ServiceRequest(InventoryService.NAME, "audit", Map.of()));
+
+ assertFalse(response.success());
+ assertEquals("Unknown operation: audit", response.message());
+ }
+
+ private ServiceResponse check(String sku, int quantity) {
+ return service.handle(
+ new ServiceRequest(
+ InventoryService.NAME, "checkStock", Map.of("sku", sku, "quantity", quantity)));
+ }
+
+ private ServiceResponse reserve(String sku, int quantity) {
+ return service.handle(
+ new ServiceRequest(
+ InventoryService.NAME, "reserve", Map.of("sku", sku, "quantity", quantity)));
+ }
+}
diff --git a/service-oriented-architecture/src/test/java/com/iluwatar/soa/OrderServiceTest.java b/service-oriented-architecture/src/test/java/com/iluwatar/soa/OrderServiceTest.java
new file mode 100644
index 000000000000..89e9b81427c5
--- /dev/null
+++ b/service-oriented-architecture/src/test/java/com/iluwatar/soa/OrderServiceTest.java
@@ -0,0 +1,187 @@
+/*
+ * 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.soa;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Map;
+import java.util.Set;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class OrderServiceTest {
+
+ private static final String CREDENTIAL = "checkout-key";
+
+ private ServiceBus bus;
+ private InventoryService inventory;
+
+ @BeforeEach
+ void setUp() {
+ var registry = new ServiceRegistry();
+ bus =
+ new ServiceBus(registry, new AccessPolicy(Set.of(CREDENTIAL), Set.of(PaymentService.NAME)));
+ inventory = new InventoryService(Map.of("LAPTOP", 2));
+ registry.register(new CustomerService());
+ registry.register(inventory);
+ registry.register(new PaymentService(1000.0));
+ registry.register(new OrderService(bus));
+ }
+
+ @Test
+ void shouldPlaceOrderAndReserveStock() {
+ var response = placeOrder("C-1", "LAPTOP", 2, 899.0, CREDENTIAL);
+
+ assertTrue(response.success());
+ var confirmation = (OrderService.OrderConfirmation) response.body();
+ assertEquals("ORD-1", confirmation.orderId());
+ assertEquals("Alice Smith", confirmation.customerName());
+ assertEquals("LAPTOP", confirmation.sku());
+ assertEquals(2, confirmation.quantity());
+ assertEquals("PAY-1", confirmation.paymentReference());
+ assertEquals(false, checkStock("LAPTOP", 1));
+ }
+
+ @Test
+ void shouldRejectOrderForUnknownCustomer() {
+ var response = placeOrder("C-9", "LAPTOP", 1, 899.0, CREDENTIAL);
+
+ assertFalse(response.success());
+ assertEquals("Order rejected: Unknown customer: C-9", response.message());
+ assertEquals(true, checkStock("LAPTOP", 2));
+ }
+
+ @Test
+ void shouldRejectOrderWhenStockIsInsufficient() {
+ var response = placeOrder("C-1", "LAPTOP", 3, 899.0, CREDENTIAL);
+
+ assertFalse(response.success());
+ assertEquals("Order rejected: insufficient stock for LAPTOP", response.message());
+ assertEquals(true, checkStock("LAPTOP", 2));
+ }
+
+ @Test
+ void shouldRejectOrderWhenPaymentIsDeclined() {
+ var response = placeOrder("C-1", "LAPTOP", 1, 1500.0, CREDENTIAL);
+
+ assertFalse(response.success());
+ assertEquals(
+ "Order rejected: Payment of 1500.0 declined for C-1: exceeds credit limit",
+ response.message());
+ assertEquals(true, checkStock("LAPTOP", 2));
+ }
+
+ @Test
+ void shouldRejectOrderWhenCallerLacksPaymentCredential() {
+ var response = placeOrder("C-1", "LAPTOP", 1, 899.0, null);
+
+ assertFalse(response.success());
+ assertEquals("Order rejected: Access denied to payment", response.message());
+ assertEquals(true, checkStock("LAPTOP", 2));
+ }
+
+ @Test
+ void shouldRejectOrderWhenReservationFails() {
+ var stubBus =
+ busWithInventoryStub(ServiceResponse.ok(true), ServiceResponse.error("reservation failed"));
+
+ var response = placeOrder(stubBus, "C-1", "LAPTOP", 1, 899.0, CREDENTIAL);
+
+ assertFalse(response.success());
+ assertEquals("Order rejected: reservation failed", response.message());
+ }
+
+ @Test
+ void shouldRejectOrderWhenStockCheckFails() {
+ var stubBus =
+ busWithInventoryStub(ServiceResponse.error("inventory unavailable"), ServiceResponse.ok(0));
+
+ var response = placeOrder(stubBus, "C-1", "LAPTOP", 1, 899.0, CREDENTIAL);
+
+ assertFalse(response.success());
+ assertEquals("Order rejected: insufficient stock for LAPTOP", response.message());
+ }
+
+ @Test
+ void shouldFailForUnknownOperation() {
+ var response = bus.send(new ServiceRequest(OrderService.NAME, "cancelOrder", Map.of()));
+
+ assertFalse(response.success());
+ assertEquals("Unknown operation: cancelOrder", response.message());
+ }
+
+ private static ServiceBus busWithInventoryStub(
+ ServiceResponse checkStockResponse, ServiceResponse reserveResponse) {
+ var registry = new ServiceRegistry();
+ var stubBus =
+ new ServiceBus(registry, new AccessPolicy(Set.of(CREDENTIAL), Set.of(PaymentService.NAME)));
+ registry.register(new CustomerService());
+ registry.register(new PaymentService(1000.0));
+ registry.register(new OrderService(stubBus));
+ registry.register(
+ new Service() {
+ @Override
+ public String name() {
+ return "inventory";
+ }
+
+ @Override
+ public ServiceResponse handle(ServiceRequest request) {
+ return "checkStock".equals(request.operation()) ? checkStockResponse : reserveResponse;
+ }
+ });
+ return stubBus;
+ }
+
+ private ServiceResponse placeOrder(
+ String customerId, String sku, int quantity, double amount, String credential) {
+ return placeOrder(bus, customerId, sku, quantity, amount, credential);
+ }
+
+ private static ServiceResponse placeOrder(
+ ServiceBus target,
+ String customerId,
+ String sku,
+ int quantity,
+ double amount,
+ String credential) {
+ return target.send(
+ new ServiceRequest(
+ OrderService.NAME,
+ "placeOrder",
+ Map.of("customerId", customerId, "sku", sku, "quantity", quantity, "amount", amount),
+ credential));
+ }
+
+ private Object checkStock(String sku, int quantity) {
+ return inventory
+ .handle(
+ new ServiceRequest(
+ InventoryService.NAME, "checkStock", Map.of("sku", sku, "quantity", quantity)))
+ .body();
+ }
+}
diff --git a/service-oriented-architecture/src/test/java/com/iluwatar/soa/PaymentServiceTest.java b/service-oriented-architecture/src/test/java/com/iluwatar/soa/PaymentServiceTest.java
new file mode 100644
index 000000000000..0b5c2efe0a07
--- /dev/null
+++ b/service-oriented-architecture/src/test/java/com/iluwatar/soa/PaymentServiceTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.soa;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+class PaymentServiceTest {
+
+ private final PaymentService service = new PaymentService(100.0);
+
+ @Test
+ void shouldChargeWithinCreditLimitAndIssueUniqueReferences() {
+ var first = charge(60.0);
+ var second = charge(100.0);
+
+ assertTrue(first.success());
+ assertTrue(second.success());
+ assertEquals("PAY-1", first.body());
+ assertEquals("PAY-2", second.body());
+ }
+
+ @Test
+ void shouldDeclineChargeAboveCreditLimit() {
+ var response = charge(100.5);
+
+ assertFalse(response.success());
+ assertEquals("Payment of 100.5 declined for C-1: exceeds credit limit", response.message());
+ }
+
+ @Test
+ void shouldDeclineNonPositiveAmount() {
+ var response = charge(0.0);
+
+ assertFalse(response.success());
+ assertEquals("Amount must be positive: 0.0", response.message());
+ }
+
+ @Test
+ void shouldFailForUnknownOperation() {
+ var response = service.handle(new ServiceRequest(PaymentService.NAME, "refund", Map.of()));
+
+ assertFalse(response.success());
+ assertEquals("Unknown operation: refund", response.message());
+ }
+
+ private ServiceResponse charge(double amount) {
+ return service.handle(
+ new ServiceRequest(
+ PaymentService.NAME, "charge", Map.of("customerId", "C-1", "amount", amount)));
+ }
+}
diff --git a/service-oriented-architecture/src/test/java/com/iluwatar/soa/ServiceBusTest.java b/service-oriented-architecture/src/test/java/com/iluwatar/soa/ServiceBusTest.java
new file mode 100644
index 000000000000..7261641f1b1c
--- /dev/null
+++ b/service-oriented-architecture/src/test/java/com/iluwatar/soa/ServiceBusTest.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.soa;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class ServiceBusTest {
+
+ private ServiceRegistry registry;
+ private ServiceBus bus;
+
+ @BeforeEach
+ void setUp() {
+ registry = new ServiceRegistry();
+ bus = new ServiceBus(registry);
+ }
+
+ @Test
+ void shouldRouteRequestToRegisteredServiceWithoutAlteringResponse() {
+ var customerService = new CustomerService();
+ registry.register(customerService);
+ var request =
+ new ServiceRequest(CustomerService.NAME, "getCustomer", Map.of("customerId", "C-1"));
+
+ var viaBus = bus.send(request);
+ var direct = customerService.handle(request);
+
+ assertEquals(direct, viaBus);
+ assertTrue(viaBus.success());
+ assertEquals("Alice Smith", ((Customer) viaBus.body()).name());
+ }
+
+ @Test
+ void shouldReturnErrorForUnknownService() {
+ var response = bus.send(new ServiceRequest("shipping", "ship", Map.of()));
+
+ assertFalse(response.success());
+ assertEquals("No such service: shipping", response.message());
+ }
+
+ @Test
+ void shouldTranslateServiceExceptionIntoErrorResponse() {
+ registry.register(new CustomerService());
+
+ var response = bus.send(new ServiceRequest(CustomerService.NAME, "getCustomer", Map.of()));
+
+ assertFalse(response.success());
+ assertTrue(response.message().startsWith("Service customer failed:"));
+ assertTrue(response.message().contains("customerId"));
+ }
+
+ @Test
+ void shouldDenyProtectedServiceWithoutInvokingIt() {
+ var invocations = new AtomicInteger();
+ registry.register(countingService(invocations));
+ var securedBus = new ServiceBus(registry, new AccessPolicy(Set.of("key"), Set.of("counter")));
+
+ var response = securedBus.send(new ServiceRequest("counter", "count", Map.of()));
+
+ assertFalse(response.success());
+ assertEquals("Access denied to counter", response.message());
+ assertEquals(0, invocations.get());
+ }
+
+ @Test
+ void shouldDenyProtectedServiceWithWrongCredential() {
+ var invocations = new AtomicInteger();
+ registry.register(countingService(invocations));
+ var securedBus = new ServiceBus(registry, new AccessPolicy(Set.of("key"), Set.of("counter")));
+
+ var response = securedBus.send(new ServiceRequest("counter", "count", Map.of(), "wrong-key"));
+
+ assertFalse(response.success());
+ assertEquals("Access denied to counter", response.message());
+ assertEquals(0, invocations.get());
+ }
+
+ @Test
+ void shouldPassValidCredentialThroughToProtectedService() {
+ var invocations = new AtomicInteger();
+ registry.register(countingService(invocations));
+ var securedBus = new ServiceBus(registry, new AccessPolicy(Set.of("key"), Set.of("counter")));
+
+ var response = securedBus.send(new ServiceRequest("counter", "count", Map.of(), "key"));
+
+ assertTrue(response.success());
+ assertEquals(1, response.body());
+ assertEquals(1, invocations.get());
+ }
+
+ private static Service countingService(AtomicInteger invocations) {
+ return new Service() {
+ @Override
+ public String name() {
+ return "counter";
+ }
+
+ @Override
+ public ServiceResponse handle(ServiceRequest request) {
+ return ServiceResponse.ok(invocations.incrementAndGet());
+ }
+ };
+ }
+}
diff --git a/service-oriented-architecture/src/test/java/com/iluwatar/soa/ServiceRegistryTest.java b/service-oriented-architecture/src/test/java/com/iluwatar/soa/ServiceRegistryTest.java
new file mode 100644
index 000000000000..ce1439c67d5d
--- /dev/null
+++ b/service-oriented-architecture/src/test/java/com/iluwatar/soa/ServiceRegistryTest.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.soa;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Set;
+import org.junit.jupiter.api.Test;
+
+class ServiceRegistryTest {
+
+ private final ServiceRegistry registry = new ServiceRegistry();
+
+ @Test
+ void shouldLookupRegisteredService() {
+ var service = new CustomerService();
+ registry.register(service);
+
+ assertSame(service, registry.lookup(CustomerService.NAME).orElseThrow());
+ assertEquals(Set.of(CustomerService.NAME), registry.serviceNames());
+ }
+
+ @Test
+ void shouldReturnEmptyForUnknownService() {
+ assertTrue(registry.lookup("missing").isEmpty());
+ }
+
+ @Test
+ void shouldRejectDuplicateRegistration() {
+ registry.register(new PaymentService());
+
+ var exception =
+ assertThrows(IllegalStateException.class, () -> registry.register(new PaymentService()));
+ assertEquals("Service already registered: payment", exception.getMessage());
+ }
+}
+ *
+ *
+ *
+ *
+ */
+public class CustomerService implements Service {
+
+ public static final String NAME = "customer";
+
+ private final Map
+ *
+ */
+public class InventoryService implements Service {
+
+ public static final String NAME = "inventory";
+
+ private final Map
+ *
+ */
+@RequiredArgsConstructor
+public class OrderService implements Service {
+
+ public static final String NAME = "order";
+
+ private static final String CUSTOMER_SERVICE = "customer";
+ private static final String INVENTORY_SERVICE = "inventory";
+ private static final String PAYMENT_SERVICE = "payment";
+
+ private final ServiceBus bus;
+ private final AtomicInteger sequence = new AtomicInteger();
+
+ /**
+ * The result of a successfully placed order.
+ *
+ * @param orderId the generated order identifier
+ * @param customerName the name of the ordering customer
+ * @param sku the ordered product
+ * @param quantity the ordered quantity
+ * @param paymentReference the reference returned by the payment service
+ */
+ public record OrderConfirmation(
+ String orderId, String customerName, String sku, int quantity, String paymentReference) {}
+
+ @Override
+ public String name() {
+ return NAME;
+ }
+
+ @Override
+ public ServiceResponse handle(ServiceRequest request) {
+ return switch (request.operation()) {
+ case "placeOrder" -> placeOrder(request);
+ default -> ServiceResponse.error("Unknown operation: " + request.operation());
+ };
+ }
+
+ private ServiceResponse placeOrder(ServiceRequest request) {
+ var customerId = request.param("customerId", String.class);
+ var sku = request.param("sku", String.class);
+ var quantity = request.param("quantity", Integer.class);
+ var amount = request.param("amount", Double.class);
+ var credential = request.credential();
+
+ var customer =
+ bus.send(
+ new ServiceRequest(
+ CUSTOMER_SERVICE, "getCustomer", Map.of("customerId", customerId), credential));
+ if (!customer.success()) {
+ return ServiceResponse.error("Order rejected: " + customer.message());
+ }
+
+ var stock =
+ bus.send(
+ new ServiceRequest(
+ INVENTORY_SERVICE,
+ "checkStock",
+ Map.of("sku", sku, "quantity", quantity),
+ credential));
+ if (!stock.success() || !Boolean.TRUE.equals(stock.body())) {
+ return ServiceResponse.error("Order rejected: insufficient stock for " + sku);
+ }
+
+ var payment =
+ bus.send(
+ new ServiceRequest(
+ PAYMENT_SERVICE,
+ "charge",
+ Map.of("customerId", customerId, "amount", amount),
+ credential));
+ if (!payment.success()) {
+ return ServiceResponse.error("Order rejected: " + payment.message());
+ }
+
+ var reservation =
+ bus.send(
+ new ServiceRequest(
+ INVENTORY_SERVICE,
+ "reserve",
+ Map.of("sku", sku, "quantity", quantity),
+ credential));
+ if (!reservation.success()) {
+ return ServiceResponse.error("Order rejected: " + reservation.message());
+ }
+
+ var confirmation =
+ new OrderConfirmation(
+ "ORD-" + sequence.incrementAndGet(),
+ ((Customer) customer.body()).name(),
+ sku,
+ quantity,
+ (String) payment.body());
+ return ServiceResponse.ok(confirmation);
+ }
+}
diff --git a/service-oriented-architecture/src/main/java/com/iluwatar/soa/PaymentService.java b/service-oriented-architecture/src/main/java/com/iluwatar/soa/PaymentService.java
new file mode 100644
index 000000000000..4ffd3a034e75
--- /dev/null
+++ b/service-oriented-architecture/src/main/java/com/iluwatar/soa/PaymentService.java
@@ -0,0 +1,80 @@
+/*
+ * 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.soa;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Enterprise service that charges customers.
+ *
+ *
+ *
+ */
+public class PaymentService implements Service {
+
+ public static final String NAME = "payment";
+
+ private final double creditLimit;
+ private final AtomicInteger sequence = new AtomicInteger();
+
+ /** Creates the service with a default credit limit. */
+ public PaymentService() {
+ this(1000.0);
+ }
+
+ /** Creates the service that declines any charge above the given limit. */
+ public PaymentService(double creditLimit) {
+ this.creditLimit = creditLimit;
+ }
+
+ @Override
+ public String name() {
+ return NAME;
+ }
+
+ @Override
+ public ServiceResponse handle(ServiceRequest request) {
+ return switch (request.operation()) {
+ case "charge" -> charge(
+ request.param("customerId", String.class), request.param("amount", Double.class));
+ default -> ServiceResponse.error("Unknown operation: " + request.operation());
+ };
+ }
+
+ private ServiceResponse charge(String customerId, double amount) {
+ if (amount <= 0) {
+ return ServiceResponse.error("Amount must be positive: " + amount);
+ }
+ if (amount > creditLimit) {
+ return ServiceResponse.error(
+ "Payment of " + amount + " declined for " + customerId + ": exceeds credit limit");
+ }
+ return ServiceResponse.ok("PAY-" + sequence.incrementAndGet());
+ }
+}
diff --git a/service-oriented-architecture/src/main/java/com/iluwatar/soa/Service.java b/service-oriented-architecture/src/main/java/com/iluwatar/soa/Service.java
new file mode 100644
index 000000000000..a9e11076c2bd
--- /dev/null
+++ b/service-oriented-architecture/src/main/java/com/iluwatar/soa/Service.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.soa;
+
+/**
+ * The service contract every provider exposes to the {@link ServiceBus}.
+ *
+ *