RxJava en CompletableFutures binnen enterprise applicaties

Geschreven door Remco Hoetmer op 12 april 2017

Schaalbare java architecturen zijn hot. Reactive programming wint aan terrein; het biedt een prachtige basis voor asynchrone verwerking. Maar hoe past het binnen een bestaande enterprise architectuur?

Om een systeem te kunnen opschalen moet taken asynchroon kunnen werken. Wie zich erin verdiept ziet overduidelijk de motivatie erachter, maar een cruciale vraag hierbij is: kunnen bestaande enterprise applicaties überhaupt wel asynchroon werken? En hoe komen die transactionele applicaties er dan uit te zien? Wat betekent het voor development, debugging en support?

De achterliggende gedachte is dat enterprise Applications toch anders zijn:

  • Veel logica, meerdere architecturele patterns
  • Veelvuldig gebruik van de database en externe systemen
  • Veranderingen en aanpassingen, vaag getuigend van veranderende inzichten over de tijd. Tijdelijke oplossingen zijn permanent gemaakt.

Om die vraag te kunnen beantwoorden heb ik typisch enterprise proces bij de hand genomen. Daarvan heb ik de "besturingslaag" in Java geïmplementeerd op de conventionele manier. Vervolgens is dezelfde logica asynchroon geïmplementeerd op 2 manieren:

  1. Java 8 Completable Futures
  2. RxJava 2 (Netflix Reactive Extensions)

De verzonnen casus betreft een bestellingsproces van Webshop. Het server process werkt transactioneel en verwerkt Web-requests van klanten:

  • Het proces wordt uitgevoerd nadat de klant items heeft geselecteerd en betaald. Het heeft 1 input en 1 output
  • Het proces doet validatie en de bestelling bij extern systeem.
  • De processtappen hebben verbinding met de DB/externe systemen. Het maakt gebruik van caching.
  • Er zitten verschillende "takken" in de verwerking, inclusief conditionele.

In schemavorm is het als volgt. De oranje blokken zijn de taken, de groene zijn koppelingen.

De Java Implementatie in verloopt common patterns. Database en background systemen worden aangeroepen via services. In de code worden implementatie-details netjes afgeschermd, waardoor de business logica duidelijk herkenbaar is.

Anders wordt het als we asynchroon gaan werken. Iedere library voor asynchronous handling werkt anders maar de overeenkomst is dat er een loskoppeling is tussen:

  • de specificatie, waarbij de taken aan elkaar worden geknoopt, en
  • de uitvoering waarbij de taken concrete data opleveren

Juist het feit dat die twee zaken bij synchrone programma's samenkomen, maakt het werken ermee handig. Bijvoorbeeld bij debugging: een synchroon programma kan je stopzetten, en vervolgens de call-stack inspecteren (het volledige pad dat het programma tot dan toe heeft gevolgd), samen met de variabelen ervan. Bij asynchrone programma's is die "overkoepelende structuur" verdwenen.

Een andere overeenkomst tussen de verschillende asynchrone libraries is dat er altijd een "container" wordt gebruikt waar data uit komt of in wordt gezet. Die container is de interface tussen de taken, soms "Future" of "Promise" genoemd. Soms is deze expliciet, maar vaak impliciet.

De eerste asynchrone implementatie is met de CompletableFuture class. Door deze Java 8 verbetering van de oude Java Future class is het mogelijk taken aan elkaar te knopen, ze zijn nu composable. Dit is ook precies wat we nodig hebben voor ons enterprise proces: sequentiële, parallelle, conditionele, samenvoegen, etc. De Java code is:

Zonder op de details in te gaan valt meteen op dat het een stuk ingewikkelder is:

  • Het specificeren van taken in containers "CompletableFuture" heeft wel wat om het lijf.
  • Introductie van sub-processen: te herkennen aan "nesting".
  • Error handling en het "completen" van alle taken. Op de achtergrond speelt verder nog het onhandige "flatmapping".

Desalniettemin, het voorbeeld bewijst dat het wel mogelijk om een proces geheel asynchroon uit te voeren. Dat is op zich een mooi resultaat, want dat is wel een van de randvoorwaarden voor opschalen.

De tweede implementatie is met de RxJava library en is van een hele andere orde. RxJava komt voort uit de reactive wereld en blinkt uit het het verwerken van streams van events. CompletableFutures werken volgens het push-model, RxJava combineert push-model met pull-model. En de hierboven genoemde container heet Observable en daar kunnen data-elementen doorlopend aan worden toegevoegd: streaming. De Java code is:

Toegegeven: de onderscheidende feature van RxJava wordt niet eens gebruikt. En je moet door de berg details heen kunnen lezen. Maar het programma gaf mij een erg verrassend resultaat: RxJava leidt tot eenzelfde structuur als CompletableFuture. Kennelijk is dit de bloedeigen structuur van het proces, de syntax doet er niet zo heel erg veel toe.

Conclusie

Asynchrone applicaties werken goed wanneer er sprake is van een pipeline. Of tenminste, wanneer het deelprobleem wordt teruggebracht tot sequentiële proces.

Enterprise applicaties hebben weinig van datgene, transacties kunnen op interne en externe data en systemen werken. Verder kan een proces moeilijk worden omgevormd naar een pipeline. De code boven laat dat helder zien.