10

I am new to RXjava functional programming. I am writing a post endpoint with multiple conditions:

  1. When the post endpoint is hit with products
  2. If the cart for the logged in user does not exist in DB then create a new cart
  3. If the cart is already present in couch base extract that record and check the products JSON whether the given productid is already there in the products JSON.
  4. If it is not available in the products JSON the new product has to be created
  5. The cart should be updated with the newly created productId
  6. If it is present in the products JSON then fetch the product record and verify the quantity.
  7. If the quantity is same then do nothing
  8. Otherwise, update the record.

I am facing a challenge to write these if else conditions in functional programming. tried with switchifEmpty and not able to write the code in that.

Here is a sample code.

public Mono<Product> createProduct(final Tuple2<String, productdto> tuple2) {
    final Product productdto = tuple2.getT2();
    return Mono.just(tuple2.getT1())
        .map(cartRepository::findById)
        .defaultIfEmpty(cartRepository.save(
            cart.builder()
                .id(tuple2.getT1())
                .build()))
        .flatMap(cartres -> cartres)
        .flatMap(cartres -> {
             final Product product = Product.builder()
                 .id(1234)
                 .productId(productDTO.getProductId())
                 .productName(productDTO.getProductName())
                 .build();
             return productRepository.save(product)
                 .map(saveCart -> cart.builder()
                     .id(cartres.getId()).build())
                 .flatMap(cartRepository::save);
        });
    }).then(Mono.just(productDto));
}
Nikolas Charalambidis
  • 40,893
  • 16
  • 117
  • 183
Srivi
  • 243
  • 4
  • 14

2 Answers2

1

I understand that there is a complex if-else-logic that you have to implement with a reactive chain with Spring WebFlux invocations of .map(), .flatMap() or defaultIfEmpty().

These reactive operators can't always replace complex if-else-logic.

Implementing if-else-decisions must be done inside your .map() or .flatMap() handlers.

In your case, you could implement a java.util.Function<> or a simple method for each step 1..8

A first example with a function that provides you with a cart by loading it from the repo or by creating a new one can be seen below.

Furthermore, it shows how you can use the zipWith() operator to add more data to the reactive chain.

Here, the existence of the product in the database is checked and the result is passed as a boolean downstream.

Based on this boolean, you can use an if-else-statement to either return the existing product or to create a new one.


    public Mono<Product> createProduct(final Tuple2<String, Product> tuple2) {
        final Product productDTO = tuple2.getT2();
        return Mono.just(tuple2.getT1())
                //   .map(cartRepository::findById)
                //           .defaultIfEmpty(cartRepository.save(
                //               cart.builder()
                //                   .id(tuple2.getT1())
                //                   .build()))
                .flatMap(provideCart(tuple2)) // see implemented method further below

                .zipWith(productRepository.existsById(productDTO.getId()))
                .flatMap(maybeCreateProduct(productDTO)); // see implemented method further below
    }

    private Function<String, Mono<Cart>> provideCart(final Tuple2<String, Product> tuple2) {
        return id -> cartRepository.findById(id)
                .switchIfEmpty(cartRepository.save(
                        Cart.builder()
                                .id(tuple2.getT1())
                                .build()));
    }

    private Function<Tuple2<Cart, Boolean>, Mono<Product>> maybeCreateProduct(Product productDTO) {
        return cartresAndProductExists -> {
            Cart cart = cartresAndProductExists.getT1();
            Boolean productExists = cartresAndProductExists.getT2();
            if (productExists) {
                return productRepository.findById(productDTO.getProductId())
            } else {
                return productRepository.save(Product.builder()
                        .id("1234")
                        .productId(productDTO.getProductId())
                        .productName(productDTO.getProductName())
                        .build());
            }
        };
    }

One has to be careful to keep the code readable and to avoid an endless chain of reactive steps.

Extract complex steps into separate methods and maybe even rethink the process in the first place to come to a more simple and streamlined solution

Erunafailaro
  • 1,835
  • 16
  • 21
  • 1
    I think I'm missing something. the `.flatMap(provideCart(...))` I understand - but immediately following, it _seems_ like flatMap(x -> x) shouldn't be there, and the following `map(x -> x)` doesn't _do_ anything. Can you explain those a bit? – Doug Kress Apr 06 '23 at 15:27
  • 1
    I removed those lines. you are right, they don't make sense. At least the `.flatMap(cartres -> cartres)` was taken over from the initial question. – Erunafailaro Apr 11 '23 at 05:03
1

I know this isn't the best way of handling complex logic, but it's how I've been doing it for basically all my time when developing using Spring WebFlux.

The problem with if-else logic is that you can't exit out of the reactive chain. If you return something in a map call it just gets thrown into the next mapper. The only solution I see to solve this problem is using exceptions to escape the reactive chain.

That's why I extended the Exception class and created CustomException:

public class CustomException extends Exception {
    public CustomException(String s) {
        super(s);
    }
}

We can now use our CustomException to implement complex if-else logic by just throwing errors and catching them at the end of the reactive chain.

For this example I'm just gonna use a simple login endpoint to demonstrate how to use it.

Let's first create a LoginForm record to receive the request body.

public record LoginForm(String username, String password) {}

Now let's implement a simple login endpoint with a AccountRepository and then creating a session using the 'SessionRepository' (Both of these repositories aren't implemented, just imagine they exist).

@PostMapping("/login")
public Mono<String> logIn(@RequestBody LoginForm loginForm) {

    // Get the account using the username
    return accountRepository.getAccountByUsername(loginForm.username()).flatMap(account -> {
        
        // Check password
        if(!account.getPassword().equals(loginForm.password()) {

            // Return "Login failed!" using the CustomException we created
            return Mono.error(new CustomException("Login failed!"));
        }
        
        // Create session
        return sessionRepository.save(new Session(account.getId(), randomToken());
    }).map(session -> "Login successful! " + session.getToken())
        .onErrorResume(CustomException.class, e -> Mono.just(e.getMessage()))
        .onErrorReturn("Server error!");
}

I hope that those who looked at this answer could understand how I implemented some if-else logic in this example. If you have any additions please comment below it.