Last active
April 20, 2025 13:29
-
-
Save fResult/201b2384c03d44cc2f271f94028732b8 to your computer and use it in GitHub Desktop.
Optional Demo for the article: https://medium.com/p/03845513d9f5 (You can see the whole code block at the file in the bottommost.)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| public class OrderService { | |
| private final OrderRepository orderRepository; | |
| private final PaymentService paymentService; | |
| private final NotificationService notificationService; | |
| private final CustomerOrderService customerOrderService; | |
| OrderService( | |
| OrderRepository orderRepository, | |
| PaymentService paymentService, | |
| NotificationService notificationService, | |
| CustomerOrderService customerOrderService) { | |
| this.orderRepository = orderRepository; | |
| this.paymentService = paymentService; | |
| this.notificationService = notificationService; | |
| this.customerOrderService = customerOrderService; | |
| } | |
| public OrderSummary processOrder(String orderId) { | |
| System.out.println("[INFO] Processing order: " + orderId); | |
| final var orderOpt = orderRepository.findById(orderId); | |
| var orderContextOpt = orderOpt | |
| .flatMap(customerOrderService::fetchOrderWithCustomer) | |
| .flatMap(this::buildOrderContextWithValidatedPayment); | |
| // Unfortunately, in Java, `ifPresent()` method doesn't return any object, so that we cannot continue chaining | |
| orderContextOpt.ifPresent(notificationService::handleOrderNotification); | |
| return orderContextOpt | |
| .map(this::buildSummary) | |
| .orElseThrow(ExceptionUtils.throwNotFound(Order.class, orderId)); | |
| } | |
| private Optional<OrderContext> buildOrderContextWithValidatedPayment( | |
| OrderWithCustomer orderWithCustomer) { | |
| var contextOpt = paymentService | |
| .fetchPaymentByCustomerId(orderWithCustomer.customer().id()) | |
| .filter(PaymentDetails::isSuccessful) | |
| .map(this.combinePaymentWithOrder(orderWithCustomer)); | |
| contextOpt.orElseThrow(ExceptionUtils.throwPaymentFailed(orderWithCustomer.order().id())); | |
| return contextOpt; | |
| } | |
| private Function<PaymentDetails, OrderContext> combinePaymentWithOrder(OrderWithCustomer orderWithCustomer) { | |
| return payment -> new OrderContext(orderWithCustomer, payment); | |
| } | |
| private OrderSummary buildSummary(OrderContext context) { | |
| return new OrderSummary(context.order(), context.customer(), context.payment()); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| public class ScratchOptional { | |
| public static void main(String[] args) { | |
| final var orderService = buildOrderService(); | |
| // <- Assume we handle this by `@ControllerAdvice` | |
| try { | |
| // TRYIT: Change orderId to be `order-2` instead of `order-1` to see PaymentFailed error occurs | |
| final var orderSummary = orderService.processOrder("order-1"); | |
| System.out.println("Order Summary: " + orderSummary); | |
| } catch (ElementNotFoundException ex) { | |
| System.out.println("[WARN]: " + ex.getMessage()); | |
| } catch (PaymentFailedException ex) { | |
| System.err.println("[ERROR]: " + ex.getMessage()); | |
| } catch (Exception ex) { | |
| System.err.println("[Error]: Something went wrong! - " + ex.getMessage()); | |
| } | |
| // Assume we handle this by `@ControllerAdvice` -> | |
| } | |
| public static OrderService buildOrderService() { | |
| final var orderRepository = new OrderRepository(); | |
| final var customerRepository = new CustomerRepository(); | |
| final var paymentRepository = new PaymentRepository(); | |
| final var notificationService = new NotificationService(); | |
| final var paymentService = new PaymentService(paymentRepository); | |
| final var customerOrderService = new CustomerOrderService(customerRepository); | |
| return new OrderService( | |
| orderRepository, paymentService, notificationService, customerOrderService); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Basic domain record | |
| public record Customer(String id, String email) {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| public class CustomerOrderService { | |
| private final CustomerRepository customerRepository; | |
| CustomerOrderService(CustomerRepository customerRepository) { | |
| this.customerRepository = customerRepository; | |
| } | |
| public Optional<OrderWithCustomer> fetchOrderWithCustomer(Order order) { | |
| final var orderWithCustomer = customerRepository | |
| .findById(order.customerId()) | |
| .map(OrderWithCustomer.fromCustomerAndOrder(order)) | |
| .orElseThrow(ExceptionUtils.throwNotFound(Customer.class, order.customerId())); | |
| return Optional.ofNullable(orderWithCustomer); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| public class CustomerRepository { | |
| private static final Map<String, Customer> data = new HashMap<>() {{ | |
| put("cust-1", new Customer("cust-1", "email1@example.com")); | |
| put("cust-2", new Customer("cust-2", "email2@example.com")); | |
| }}; | |
| Optional<Customer> findById(String customerId) { | |
| return Optional.of(data.get(customerId)); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| public class ElementNotFoundException extends RuntimeException { | |
| public ElementNotFoundException(String message) { | |
| super(message); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| final class ExceptionUtils { | |
| public static Supplier<ElementNotFoundException> throwNotFound(Class<?> resouceClass, String resourceId) { | |
| return () -> new ElementNotFoundException(resouceClass.getSimpleName() + " with id [" + resourceId + "] not found"); | |
| } | |
| public static Supplier<ElementNotFoundException> throwSubResourceNotFound(Class<?> resourceClass, Class<?> subResourceClass, String resourceId) { | |
| final var errorMessage = String.format("%s for %s [%s] not found", subResourceClass.getSimpleName(), resourceClass.getSimpleName(), resourceId); | |
| return () -> new ElementNotFoundException(errorMessage); | |
| } | |
| public static Supplier<PaymentFailedException> throwPaymentFailed(String orderId) { | |
| final var errorMessage = String.format("Payment unsuccessful for Order with id [%s]", orderId); | |
| return () -> new PaymentFailedException(errorMessage); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| public class NotificationException extends RuntimeException { | |
| public NotificationException(String message) { | |
| super(message); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| public class NotificationService { | |
| public void handleOrderNotification(OrderContext context) { | |
| try { | |
| this.sendConfirmation(context.customer().email(), context.order().details()); | |
| } catch (NotificationException e) { | |
| final var errorMessage = "Failed to send notification, but order processed: " + e.getMessage(); | |
| // We continue processing since this is non-critical | |
| System.out.println("[WARN]: " + errorMessage); | |
| } | |
| } | |
| private void sendConfirmation(String email, OrderDetails details) throws NotificationException { | |
| System.out.println("Notification: Notified to [" + email + "] with " + details); | |
| // TRYIT: Uncomment code below to mock this method as error occurrence | |
| // final var errorMessage = String.format("Email cannot reach customer with email: [%s] by some reason", email); | |
| // throw new NotificationException(errorMessage); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Basic domain record | |
| public record Order(String id, String customerId, OrderDetails details) {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Composite record for the processing flow | |
| public record OrderContext(OrderWithCustomer orderWithCustomer, PaymentDetails payment) { | |
| // Convenience methods to access nested properties | |
| public Order order() { | |
| return orderWithCustomer.order(); | |
| } | |
| public Customer customer() { | |
| return orderWithCustomer.customer(); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| public record OrderDetails(String productId, int quantity, double amount) {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Fake Repository class | |
| public class OrderRepository { | |
| private static final Map<String, Order> data = new HashMap<>() {{ | |
| put("order-1", new Order("order-1", "cust-1", new OrderDetails("detail-1", 10, 1000))); | |
| put("order-2", new Order("order-2", "cust-2", new OrderDetails("detail-2", 50, 500))); | |
| }}; | |
| Optional<Order> findById(String orderId) { | |
| return Optional.ofNullable(data.get(orderId)); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Composite record for the processing flow | |
| public record OrderSummary(Order order, Customer customer, PaymentDetails payment) {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Composite record for the processing flow | |
| public record OrderWithCustomer(Order order, Customer customer) { | |
| public static Function<Customer, OrderWithCustomer> fromCustomerAndOrder(Order order) { | |
| return customer -> new OrderWithCustomer(order, customer); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| public record PaymentDetails(String id, String orderId, boolean isSuccessful) {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| public class PaymentFailedException extends RuntimeException { | |
| public PaymentFailedException(String message) { | |
| super(message); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| public class PaymentRepository { | |
| private static final Map<String, PaymentDetails> data = new HashMap<>() {{ | |
| put("cust-1", new PaymentDetails("payment-1", "order-1", true)); | |
| put("cust-2", new PaymentDetails("payment-2", "order-2", false)); | |
| }}; | |
| public Optional<PaymentDetails> findPaymentByCustomerId(String orderId) { | |
| return Optional.ofNullable(data.get(orderId)); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| public class PaymentService { | |
| private final PaymentRepository paymentRepository; | |
| PaymentService(PaymentRepository paymentRepository) { | |
| this.paymentRepository = paymentRepository; | |
| } | |
| public Optional<PaymentDetails> fetchPaymentByCustomerId(String customerId) { | |
| final var paymentOpt = paymentRepository.findPaymentByCustomerId(customerId); | |
| paymentOpt.orElseThrow(ExceptionUtils.throwSubResourceNotFound( | |
| Customer.class, PaymentDetails.class, customerId)); | |
| return paymentOpt; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import java.util.HashMap; | |
| import java.util.Map; | |
| import java.util.Optional; | |
| import java.util.function.Function; | |
| import java.util.function.Supplier; | |
| class OptionalDemo { | |
| public static void main(String[] args) { | |
| final var orderService = buildOrderService(); | |
| // <- Assume we handle this by `@ControllerAdvice` | |
| try { | |
| // TRYIT: Change orderId to be `order-2` instead of `order-1` to see PaymentFailed error occurs | |
| final var orderSummary = orderService.processOrder("order-1"); | |
| System.out.println("Order Summary: " + orderSummary); | |
| } catch (ElementNotFoundException ex) { | |
| System.out.println("[WARN]: " + ex.getMessage()); | |
| } catch (PaymentFailedException ex) { | |
| System.err.println("[ERROR]: " + ex.getMessage()); | |
| } catch (Exception ex) { | |
| System.err.println("[Error]: Something went wrong! - " + ex.getMessage()); | |
| } | |
| // Assume we handle this by `@ControllerAdvice` -> | |
| } | |
| public static OrderService buildOrderService() { | |
| final var orderRepository = new OrderRepository(); | |
| final var customerRepository = new CustomerRepository(); | |
| final var paymentRepository = new PaymentRepository(); | |
| final var notificationService = new NotificationService(); | |
| final var paymentService = new PaymentService(paymentRepository); | |
| final var customerOrderService = new CustomerOrderService(customerRepository); | |
| return new OrderService( | |
| orderRepository, paymentService, notificationService, customerOrderService); | |
| } | |
| } | |
| class OrderService { | |
| private final OrderRepository orderRepository; | |
| private final PaymentService paymentService; | |
| private final NotificationService notificationService; | |
| private final CustomerOrderService customerOrderService; | |
| OrderService( | |
| OrderRepository orderRepository, | |
| PaymentService paymentService, | |
| NotificationService notificationService, | |
| CustomerOrderService customerOrderService) { | |
| this.orderRepository = orderRepository; | |
| this.paymentService = paymentService; | |
| this.notificationService = notificationService; | |
| this.customerOrderService = customerOrderService; | |
| } | |
| public OrderSummary processOrder(String orderId) { | |
| System.out.println("[INFO] Processing order: " + orderId); | |
| final var orderOpt = orderRepository.findById(orderId); | |
| var orderContextOpt = orderOpt | |
| .flatMap(customerOrderService::fetchOrderWithCustomer) | |
| .flatMap(this::buildOrderContextWithValidatedPayment); | |
| // Unfortunately, in Java, `ifPresent()` method doesn't return any object, so that we cannot continue chaining | |
| orderContextOpt.ifPresent(notificationService::handleOrderNotification); | |
| return orderContextOpt | |
| .map(this::buildSummary) | |
| .orElseThrow(ExceptionUtils.throwNotFound(Order.class, orderId)); | |
| } | |
| private Optional<OrderContext> buildOrderContextWithValidatedPayment( | |
| OrderWithCustomer orderWithCustomer) { | |
| var contextOpt = paymentService | |
| .fetchPaymentByCustomerId(orderWithCustomer.customer().id()) | |
| .filter(PaymentDetails::isSuccessful) | |
| .map(this.combinePaymentWithOrder(orderWithCustomer)); | |
| contextOpt.orElseThrow(ExceptionUtils.throwPaymentFailed(orderWithCustomer.order().id())); | |
| return contextOpt; | |
| } | |
| private Function<PaymentDetails, OrderContext> combinePaymentWithOrder(OrderWithCustomer orderWithCustomer) { | |
| return payment -> new OrderContext(orderWithCustomer, payment); | |
| } | |
| private OrderSummary buildSummary(OrderContext context) { | |
| return new OrderSummary(context.order(), context.customer(), context.payment()); | |
| } | |
| } | |
| // Basic domain records | |
| record Order(String id, String customerId, OrderDetails details) {} | |
| record Customer(String id, String email) {} | |
| record PaymentDetails(String id, String orderId, boolean isSuccessful) {} | |
| record OrderDetails(String productId, int quantity, double amount) {} | |
| // Composite records for the processing flow | |
| record OrderWithCustomer(Order order, Customer customer) { | |
| public static Function<Customer, OrderWithCustomer> fromCustomerAndOrder(Order order) { | |
| return customer -> new OrderWithCustomer(order, customer); | |
| } | |
| } | |
| record OrderContext(OrderWithCustomer orderWithCustomer, PaymentDetails payment) { | |
| // Convenience methods to access nested properties | |
| public Order order() { | |
| return orderWithCustomer.order(); | |
| } | |
| public Customer customer() { | |
| return orderWithCustomer.customer(); | |
| } | |
| } | |
| record OrderSummary(Order order, Customer customer, PaymentDetails payment) {} | |
| // Exception classes | |
| class ElementNotFoundException extends RuntimeException { | |
| public ElementNotFoundException(String message) { | |
| super(message); | |
| } | |
| } | |
| class PaymentFailedException extends RuntimeException { | |
| public PaymentFailedException(String message) { | |
| super(message); | |
| } | |
| } | |
| class NotificationException extends RuntimeException { | |
| public NotificationException(String message) { | |
| super(message); | |
| } | |
| } | |
| // Repository | |
| class OrderRepository { | |
| private static final Map<String, Order> data = new HashMap<>() {{ | |
| put("order-1", new Order("order-1", "cust-1", new OrderDetails("detail-1", 10, 1000))); | |
| put("order-2", new Order("order-2", "cust-2", new OrderDetails("detail-2", 50, 500))); | |
| }}; | |
| Optional<Order> findById(String orderId) { | |
| return Optional.ofNullable(data.get(orderId)); | |
| } | |
| } | |
| class CustomerRepository { | |
| private static final Map<String, Customer> data = new HashMap<>() {{ | |
| put("cust-1", new Customer("cust-1", "email1@example.com")); | |
| put("cust-2", new Customer("cust-2", "email2@example.com")); | |
| }}; | |
| Optional<Customer> findById(String customerId) { | |
| return Optional.of(data.get(customerId)); | |
| } | |
| } | |
| class PaymentService { | |
| private final PaymentRepository paymentRepository; | |
| PaymentService(PaymentRepository paymentRepository) { | |
| this.paymentRepository = paymentRepository; | |
| } | |
| public Optional<PaymentDetails> fetchPaymentByCustomerId(String customerId) { | |
| final var paymentOpt = paymentRepository.findPaymentByCustomerId(customerId); | |
| paymentOpt.orElseThrow(ExceptionUtils.throwSubResourceNotFound( | |
| Customer.class, PaymentDetails.class, customerId)); | |
| // Unfortunately, in Java, `orElseThrow()` method returns `R` instead of of `Optional<R>`, so that we cannot continue chaining | |
| return paymentOpt; | |
| } | |
| } | |
| class NotificationService { | |
| public void handleOrderNotification(OrderContext context) { | |
| try { | |
| this.sendConfirmation(context.customer().email(), context.order().details()); | |
| } catch (NotificationException e) { | |
| final var errorMessage = "Failed to send notification, but order processed: " + e.getMessage(); | |
| // We continue processing since this is non-critical | |
| System.out.println("[WARN]: " + errorMessage); | |
| } | |
| } | |
| private void sendConfirmation(String email, OrderDetails details) throws NotificationException { | |
| System.out.println("Notification: Notified to [" + email + "] with " + details); | |
| // TRYIT: Uncomment code below to mock this method as error occurrence | |
| // final var errorMessage = String.format("Email cannot reach customer with email: [%s] by some reason", email); | |
| // throw new NotificationException(errorMessage); | |
| } | |
| } | |
| // Service classes | |
| class CustomerOrderService { | |
| private final CustomerRepository customerRepository; | |
| CustomerOrderService(CustomerRepository customerRepository) { | |
| this.customerRepository = customerRepository; | |
| } | |
| public Optional<OrderWithCustomer> fetchOrderWithCustomer(Order order) { | |
| final var orderWithCustomer = customerRepository | |
| .findById(order.customerId()) | |
| .map(OrderWithCustomer.fromCustomerAndOrder(order)) | |
| .orElseThrow(ExceptionUtils.throwNotFound(Customer.class, order.customerId())); | |
| // Unfortunately, in Java, `orElseThrow()` method returns `R` instead of of `Optional<R>`, so that we cannot continue chaining | |
| return Optional.ofNullable(orderWithCustomer); | |
| } | |
| } | |
| class PaymentRepository { | |
| private static final Map<String, PaymentDetails> data = new HashMap<>() {{ | |
| put("cust-1", new PaymentDetails("payment-1", "order-1", true)); | |
| put("cust-2", new PaymentDetails("payment-2", "order-2", false)); | |
| }}; | |
| public Optional<PaymentDetails> findPaymentByCustomerId(String customerId) { | |
| return Optional.ofNullable(data.get(customerId)); | |
| } | |
| } | |
| final class ExceptionUtils { | |
| public static Supplier<ElementNotFoundException> throwNotFound(Class<?> resouceClass, String resourceId) { | |
| return () -> new ElementNotFoundException(resouceClass.getSimpleName() + " with id [" + resourceId + "] not found"); | |
| } | |
| public static Supplier<ElementNotFoundException> throwSubResourceNotFound(Class<?> resourceClass, Class<?> subResourceClass, String resourceId) { | |
| final var errorMessage = String.format("%s for %s [%s] not found", subResourceClass.getSimpleName(), resourceClass.getSimpleName(), resourceId); | |
| return () -> new ElementNotFoundException(errorMessage); | |
| } | |
| public static Supplier<PaymentFailedException> throwPaymentFailed(String orderId) { | |
| final var errorMessage = String.format("Payment unsuccessful for Order with id [%s]", orderId); | |
| return () -> new PaymentFailedException(errorMessage); | |
| } | |
| } | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment