Code Monkey home page Code Monkey logo

redelastic / reactive-stock-trader Goto Github PK

View Code? Open in Web Editor NEW
127.0 14.0 48.0 923 KB

A reference architecture for stock trading to demonstrate the concepts of reactive systems development. Based on the original Stock Trader by IBM and implemented with Lagom by Lightbend.

Home Page: https://developer.ibm.com/languages/java/series/reactive-in-practice/

License: Apache License 2.0

Java 77.34% Scala 2.13% JavaScript 2.66% HTML 0.04% Vue 17.74% Shell 0.09%
lagom-framework reactive-programming ddd vuejs

reactive-stock-trader's Introduction

Reactive in Practice: An Introduction

Reactive in practice: A complete guide to event-driven systems development in Java is a 12 part series that takes learners through the entire spectrum of a real-world event-sourced project from inception to deployment. This reference architecture is to support the series, originally published on IBM Developer:

Complete examples of this material are very sparse in the industry because they are so time intensive to create. However, with the support of IBM and the IBM Developer portal, we've brought this material to life. We hope it inspires the community and showcases best practices of these technologies. Special thanks to Lightbend for peer review assistance.

The main technologies showcased:

  • Java
  • Lagom
  • Play
  • Kafka
  • Cassandra

Published units of learning materials

This reference architecture is meant to enhance the learning experience of Reactive in Practice, a 12 part learning series published by IBM. Please visit the learning materials below to learn about CQRS and event sourcing using Lagom and Vue.

Contributions

If you would like to contribute, fork this repo and issue a PR. All contributions are welcome!

Installation

The following will help you get set up in the following contexts:

  • Local development
  • Deployment to local Kubernetes (using Minikube)
  • Interactions (UI, command line)

Local development and running locally

Most of your interaction with Lagom will be via the command line. Please complete the following steps.

  1. Install Java 8 SDK
  2. Install sbt (brew install sbt on Mac)
  3. Sign up for a free IEX Cloud account for a sandbox stock quote service
    • IEX Cloud is used for stock quotes and historical stock data
    • Click "sandbox mode" from the main landing page to avoid production limits (see instructions on IEX Cloud for more info)
    • Update quote.iex.token="YOUR_TOKEN_HERE" with your API test key in broker-impl/src/main/resources/application.conf and application.prod.conf (test keys from IEX start with T)
  4. Sign up for a free Rollbar account and create an access token for front-end logging
    • obtain an access token
    • edit ui/.env and ensure VUE_APP_ROLLBAR_ACCESS_TOKEN is set to your token
  5. Running Lagom in development mode is simple. Start by launching the backend services using sbt.
    • sbt runAll

The BFF ("backend for frontend") exposes an API on port 9100.

Setting up an IDE

Lagom should import fine into IntelliJ IDEA and VSCode. A few pieces of advice:

  • Lagom requires Java 8 compatibility mode for compilation. We recommend installing Java 11 as your default JDK and the compilation target to Java 8. This combination will provide the best ergonomics and support.
  • Reactive Stock Trader makes heavy use of Lombok, a persistent collections framework for Java. You should find the appropriate IDE extensions for Lombok to avoid (significant) IDE warnings.

Testing the backend with CURL

Let's ensure Reactive Stock trader is running properly before wiring up the UI. Do do this, we'll use curl and jq from the command line.

The jq command line tool for JSON is very handy for pretty printing JSON responses, on Mac this can be installed with brew install jq.

The following curl commands will create a new portfolio and then place a few orders to ensure that all microservices are functioning correctly.

# open a new portfolio
PID=$(curl -X POST http:/localhost:9100/api/portfolio -F name="piggy bank savings" | jq -r .portfolioId); echo $PID

# check the portfolio (you should see a lack of funds)
curl http://localhost:9100/api/portfolio/$PID | jq .

# transfer funds into the portfolio
curl -X POST http://localhost:9100/api/transfer -F amount=20000 -F sourceType=savings -F sourceId=123 -F destinationType=portfolio -F destinationId=$PID

# check the portfolio (you should see new funds)
curl http://localhost:9100/api/portfolio/$PID | jq .

# purchase shares in IBM
curl -X POST http://localhost:9100/api/portfolio/$PID/order -F symbol=IBM -F shares=10 -F order=buy

# you should see less funds and now hold shares of IBM
curl http://localhost:9100/api/portfolio/$PID | jq .

If all backend services are configured correctly, you should see the following output:

{
  "portfolioId": "93ce709d-c15d-4277-b973-1e41c5d2be09",
  "name": "piggy bank savings",
  "funds": 18654,
  "holdings": [
    {
      "symbol": "IBM",
      "shareCount": 10,
      "marketValue": 1323
    }
  ]
}

Configuring and launching the UI

The UI is developed in Vue.js. You'll need to have Node.js and npm installed and then follow the instructions below.

Project setup and launching for development:

npm install
npm run serve

This will launch the UI on localhost:8080 for development. You can then use the UI to interact with the Lagom system.

Testing / debugging:

  • Run your tests: npm run test
  • Lints and fixes files: npm run lint

Deploying to Kubernetes

For instructions on how to deploy Reactive Stock Trader to Kubernetes, you can find the deployment instructions and Helm Charts for Kafka and Cassandra here: https://github.com/RedElastic/reactive-stock-trader/tree/master/deploy

reactive-stock-trader's People

Contributors

dana-harrington avatar rjammart avatar rocketpages avatar sfdomina avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

reactive-stock-trader's Issues

UI: Feedback from order placement

The place order call with either respond with a 202, or a 4xx error response (e.g. attempt to sell but insufficient shares available). There should be user feedback as to the status of the initial placement call (independent of the asynchronous order fulfilment process).

Clean up and use `collectByEvent` for event streams.

In the wire transfer service implementation we have a helper function collectByEvent to collect on a stream of Pair<Event, Offset> by the type of Event. It may be possible to implement this a little more clearly (and/or with fewer casts).

This functionality can also be used to simplify some code for processing event streams in other services.

Nice handling of custom exceptions (or remove them)

Handle error responses such as the InsufficientShares response received when placing a sell order without sufficient shares held. The end result should be a helpful response to the UI.

This could either be accomplished by extending the service call response type to include failure cases, or using the exception mechanism and handling the exception serialization issues.

Transfer vs. BuySell events

It would be nice to differentiate between portfolio fund events associated with a money transfer vs. a trade.

Portfolio: Use less generic events

Currently using a combination of more general events, e.g. both a trade and a wire transfer involve a FundsCredited/FundsDebited event. It would be more useful to provide more specialized events. Otherwise it can be more difficult to interpret the log unambiguously, e.g. for generating a transaction history. Prefer a single event containing all the relevant data rather than a set of events per command.

Portfolio not found (internal error)

After first creating a portfolio and then visiting the portfolio tab, this error appeared in the logs and no portfolio data was displayed on the portfolio page.

play.api.http.HttpErrorHandlerExceptions$$anon$1: Execution exception[[CompletionException: com.lightbend.lagom.javadsl.api.transport.NotFound: Portfolio undefined not found. (TransportErrorCode{http=404, webSocket=1008, description='Policy Violation/Not Found'})]]
	at play.api.http.HttpErrorHandlerExceptions$.throwableToUsefulException(HttpErrorHandler.scala:251)
	at play.api.http.HttpErrorHandlerExceptions.throwableToUsefulException(HttpErrorHandler.scala)
	at play.http.DefaultHttpErrorHandler.throwableToUsefulException(DefaultHttpErrorHandler.java:204)
	at play.http.DefaultHttpErrorHandler.onServerError(DefaultHttpErrorHandler.java:164)
	at play.core.j.JavaHttpErrorHandlerAdapter.$anonfun$onServerError$1(JavaHttpErrorHandlerAdapter.scala:22)
	at play.core.j.JavaHelpers.$anonfun$invokeWithContext$1(JavaHelpers.scala:273)
	at play.core.j.JavaHelpers.withContext(JavaHelpers.scala:284)
	at play.core.j.JavaHelpers.withContext$(JavaHelpers.scala:280)
	at play.core.j.JavaHelpers$.withContext(JavaHelpers.scala:293)
	at play.core.j.JavaHelpers.invokeWithContext(JavaHelpers.scala:272)
	at play.core.j.JavaHelpers.invokeWithContext$(JavaHelpers.scala:271)
	at play.core.j.JavaHelpers$.invokeWithContext(JavaHelpers.scala:293)
	at play.core.j.JavaHttpErrorHandlerAdapter.onServerError(JavaHttpErrorHandlerAdapter.scala:22)
	at play.filters.cors.AbstractCORSPolicy$$anonfun$1.applyOrElse(AbstractCORSPolicy.scala:125)
	at play.filters.cors.AbstractCORSPolicy$$anonfun$1.applyOrElse(AbstractCORSPolicy.scala:123)
	at scala.concurrent.Future.$anonfun$recoverWith$1(Future.scala:414)
	at scala.concurrent.impl.Promise.$anonfun$transformWith$1(Promise.scala:37)
	at scala.concurrent.impl.CallbackRunnable.run(Promise.scala:60)
	at play.api.libs.streams.Execution$trampoline$.executeScheduled(Execution.scala:109)
	at play.api.libs.streams.Execution$trampoline$.execute(Execution.scala:71)
	at scala.concurrent.impl.CallbackRunnable.executeWithValue(Promise.scala:68)
	at scala.concurrent.impl.Promise$DefaultPromise.$anonfun$tryComplete$1(Promise.scala:284)
	at scala.concurrent.impl.Promise$DefaultPromise.$anonfun$tryComplete$1$adapted(Promise.scala:284)
	at scala.concurrent.impl.Promise$DefaultPromise.tryComplete(Promise.scala:284)
	at scala.concurrent.Promise.complete(Promise.scala:49)
	at scala.concurrent.Promise.complete$(Promise.scala:48)
	at scala.concurrent.impl.Promise$DefaultPromise.complete(Promise.scala:183)
	at scala.concurrent.java8.FuturesConvertersImpl$P.accept(FutureConvertersImpl.scala:94)
	at scala.concurrent.java8.FuturesConvertersImpl$P.accept(FutureConvertersImpl.scala:89)
	at java.util.concurrent.CompletableFuture.uniWhenComplete(CompletableFuture.java:760)
	at java.util.concurrent.CompletableFuture$UniWhenComplete.tryFire(CompletableFuture.java:736)
	at java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:474)
	at java.util.concurrent.CompletableFuture.completeExceptionally(CompletableFuture.java:1977)
	at scala.concurrent.java8.FuturesConvertersImpl$CF.apply(FutureConvertersImpl.scala:21)
	at scala.concurrent.java8.FuturesConvertersImpl$CF.apply(FutureConvertersImpl.scala:18)
	at scala.concurrent.impl.CallbackRunnable.run(Promise.scala:60)
	at scala.concurrent.BatchingExecutor$Batch.processBatch$1(BatchingExecutor.scala:63)
	at scala.concurrent.BatchingExecutor$Batch.$anonfun$run$1(BatchingExecutor.scala:78)
	at scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:12)
	at scala.concurrent.BlockContext$.withBlockContext(BlockContext.scala:81)
	at scala.concurrent.BatchingExecutor$Batch.run(BatchingExecutor.scala:55)
	at scala.concurrent.Future$InternalCallbackExecutor$.unbatchedExecute(Future.scala:866)
	at scala.concurrent.BatchingExecutor.execute(BatchingExecutor.scala:106)
	at scala.concurrent.BatchingExecutor.execute$(BatchingExecutor.scala:103)
	at scala.concurrent.Future$InternalCallbackExecutor$.execute(Future.scala:864)
	at scala.concurrent.impl.CallbackRunnable.executeWithValue(Promise.scala:68)
	at scala.concurrent.impl.Promise$DefaultPromise.$anonfun$tryComplete$1(Promise.scala:284)
	at scala.concurrent.impl.Promise$DefaultPromise.$anonfun$tryComplete$1$adapted(Promise.scala:284)
	at scala.concurrent.impl.Promise$DefaultPromise.tryComplete(Promise.scala:284)
	at scala.concurrent.Promise.complete(Promise.scala:49)
	at scala.concurrent.Promise.complete$(Promise.scala:48)
	at scala.concurrent.impl.Promise$DefaultPromise.complete(Promise.scala:183)
	at scala.concurrent.impl.Promise.$anonfun$transform$1(Promise.scala:29)
	at scala.concurrent.impl.CallbackRunnable.run(Promise.scala:60)
	at akka.dispatch.BatchingExecutor$AbstractBatch.processBatch(BatchingExecutor.scala:55)
	at akka.dispatch.BatchingExecutor$BlockableBatch.$anonfun$run$1(BatchingExecutor.scala:91)
	at scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:12)
	at scala.concurrent.BlockContext$.withBlockContext(BlockContext.scala:81)
	at akka.dispatch.BatchingExecutor$BlockableBatch.run(BatchingExecutor.scala:91)
	at akka.dispatch.TaskInvocation.run(AbstractDispatcher.scala:40)
	at akka.dispatch.ForkJoinExecutorConfigurator$AkkaForkJoinTask.exec(ForkJoinExecutorConfigurator.scala:44)
	at akka.dispatch.forkjoin.ForkJoinTask.doExec(ForkJoinTask.java:260)
	at akka.dispatch.forkjoin.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1339)
	at akka.dispatch.forkjoin.ForkJoinPool.runWorker(ForkJoinPool.java:1979)
	at akka.dispatch.forkjoin.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:107)
Caused by: java.util.concurrent.CompletionException: com.lightbend.lagom.javadsl.api.transport.NotFound: Portfolio undefined not found. (TransportErrorCode{http=404, webSocket=1008, description='Policy Violation/Not Found'})
	at java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:292)
	at java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:308)
	at java.util.concurrent.CompletableFuture.uniApply(CompletableFuture.java:593)
	at java.util.concurrent.CompletableFuture$UniApply.tryFire(CompletableFuture.java:577)
	at java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:474)
	at java.util.concurrent.CompletableFuture.completeExceptionally(CompletableFuture.java:1977)
	at scala.concurrent.java8.FuturesConvertersImpl$CF.apply(FutureConvertersImpl.scala:21)
	at scala.concurrent.java8.FuturesConvertersImpl$CF.apply(FutureConvertersImpl.scala:18)
	at scala.concurrent.impl.CallbackRunnable.run(Promise.scala:60)
	at scala.concurrent.BatchingExecutor$Batch.processBatch$1(BatchingExecutor.scala:63)
	at scala.concurrent.BatchingExecutor$Batch.$anonfun$run$1(BatchingExecutor.scala:78)
	at scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:12)
	at scala.concurrent.BlockContext$.withBlockContext(BlockContext.scala:81)
	at scala.concurrent.BatchingExecutor$Batch.run(BatchingExecutor.scala:55)
	at scala.concurrent.Future$InternalCallbackExecutor$.unbatchedExecute(Future.scala:866)
	at scala.concurrent.BatchingExecutor.execute(BatchingExecutor.scala:106)
	at scala.concurrent.BatchingExecutor.execute$(BatchingExecutor.scala:103)
	at scala.concurrent.Future$InternalCallbackExecutor$.execute(Future.scala:864)
	at scala.concurrent.impl.CallbackRunnable.executeWithValue(Promise.scala:68)
	at scala.concurrent.impl.Promise$DefaultPromise.$anonfun$tryComplete$1(Promise.scala:284)
	at scala.concurrent.impl.Promise$DefaultPromise.$anonfun$tryComplete$1$adapted(Promise.scala:284)
	at scala.concurrent.impl.Promise$DefaultPromise.tryComplete(Promise.scala:284)
	at scala.concurrent.Promise.tryFailure(Promise.scala:108)
	at scala.concurrent.Promise.tryFailure$(Promise.scala:108)
	at scala.concurrent.impl.Promise$DefaultPromise.tryFailure(Promise.scala:183)
	at akka.pattern.CircuitBreaker$State.$anonfun$callThrough$4(CircuitBreaker.scala:760)
	at akka.pattern.CircuitBreaker$State.$anonfun$callThrough$4$adapted(CircuitBreaker.scala:755)
	at scala.concurrent.impl.CallbackRunnable.run(Promise.scala:60)
	at akka.dispatch.BatchingExecutor$AbstractBatch.processBatch(BatchingExecutor.scala:55)
	at akka.dispatch.BatchingExecutor$Batch.run(BatchingExecutor.scala:73)
	at akka.dispatch.ExecutionContexts$sameThreadExecutionContext$.unbatchedExecute(Future.scala:77)
	at akka.dispatch.BatchingExecutor.execute(BatchingExecutor.scala:120)
	at akka.dispatch.BatchingExecutor.execute$(BatchingExecutor.scala:114)
	at akka.dispatch.ExecutionContexts$sameThreadExecutionContext$.execute(Future.scala:76)
	... 20 common frames omitted
Caused by: com.lightbend.lagom.javadsl.api.transport.NotFound: Portfolio undefined not found.
	at com.lightbend.lagom.javadsl.api.transport.TransportException.fromCodeAndMessage(TransportException.java:46)
	at com.lightbend.lagom.javadsl.jackson.JacksonExceptionSerializer.deserialize(JacksonExceptionSerializer.java:105)
	at com.lightbend.lagom.internal.javadsl.client.JavadslServiceApiBridge.exceptionSerializerDeserializeHttpException(JavadslServiceApiBridge.scala:103)
	at com.lightbend.lagom.internal.javadsl.client.JavadslServiceApiBridge.exceptionSerializerDeserializeHttpException$(JavadslServiceApiBridge.scala:101)
	at com.lightbend.lagom.internal.javadsl.client.JavadslClientServiceCallInvoker.exceptionSerializerDeserializeHttpException(JavadslServiceClientImplementor.scala:135)
	at com.lightbend.lagom.internal.javadsl.client.JavadslClientServiceCallInvoker.exceptionSerializerDeserializeHttpException(JavadslServiceClientImplementor.scala:135)
	at com.lightbend.lagom.internal.client.ClientServiceCallInvoker.$anonfun$makeStrictCall$3(ClientServiceCallInvoker.scala:222)
	at scala.util.Success.$anonfun$map$1(Try.scala:251)
	at scala.util.Success.map(Try.scala:209)
	at scala.concurrent.Future.$anonfun$map$1(Future.scala:289)
	at scala.concurrent.impl.Promise.liftedTree1$1(Promise.scala:29)
	... 13 common frames omitted

Portfolio: Order history

Need to maintain recent order progress to support idempotence (and secondarily as a means to provide an order history view).

Fix issue if active portfolio is no longer valid

play.api.http.HttpErrorHandlerExceptions$$anon$1: Execution exception[[CompletionException: com.lightbend.lagom.javadsl.api.transport.NotFound: Portfolio 0b1f1917-59c6-412a-8563-5af30708df9c not found. (TransportErrorCode{http=404, webSocket=1008, description='Policy Violation/Not Found'})]]

Document shared libraries (`commonModels` and `utils`)

Shared libraries are considered by some to be an anti-pattern in microservices, so we should aim to clear up confusion on shared code between bounded contexts. This looks like commonModels and utils. A quick item in the README should do the trick, along with some advice on how to treat both packages if refactored to physically separate microservice repos (e.g, do those become "shared libs" and published as versioned artefacts?)

Wire transfers: Implement SSE for an enhanced user experience

Currently, the UI is not fully 'reactive' in the sense that an action requires a full page refresh to update the UI.

Consider transferring funds. This will take a moment to complete on the server side. However, the user's balance (cash on hand) is not updated on the UI until the next page refresh. This makes it look like the button click either failed or did nothing.

The same mechanism should be applied to most of the UI for all commands.

Handle errors in PortfolioController

There are a few lines within PortfolioController that are not safe, specifically:

PlaceOrderForm orderForm = placeOrderForm.bindFromRequest().get();

and

OpenPortfolioDetails openRequest = openPortfolioForm.bindFromRequest().get().toRequest();

We'll need to add error handling here before this goes live.

Portfolio: Transaction history (read side driven)

Keep a regular Cassandra table of transactions (orders/trades and transfers) to provide an 'transactions' view.

This would demonstrate:

  • CQRS through a Lagom read-side processor
  • non-event sourced persistence

This would directly drive the trading and transaction history view in the UI.

BFF: Get current stock values for portfolio view

There is some logic left in the portfolio service to do this, move it into the BFF and integrate it there. This code can be used to illustrate various CompletionStage operations (e.g. fan out and error handling).

Notify user after portfolio created

Currently there is no feedback to the user once a portfolio has been created. We should have a spinner and some type of alert once the response from server has been received.

Portfolio: Add and implement close portfolio command

Close should put the account into a closed state, and should only be accepted if the portfolio is empty (funds = 0, holdings are empty, no active orders or transfers). This is a simpler feature than liquidate, which would automate the process of emptying the portfolio before closing it.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.