/* The idea of this coding challenge, is to implement a service able to subscribe to upstream balance change notifications from the GeneralBalanceService, keep an accumulated running balance for each customer, and send downstream balance update notifications for subscribed customers. Steps: +-----------+ +-------------------------+ +-----------------------+ | Customer | | CustomerBalanceService | | GeneralBalanceService | +-----------+ +-------------------------+ +-----------------------+ | | -------------------------------\ | | |-| ::new(GeneralBalanceService) | | | | |------------------------------| | | | | | | subscribe(GeneralBalanceUpdateCallback) | | |---------------------------------------------------->| | | | | subscribe(CustomerBalanceUpdateCallback) | | |------------------------------------------------------>| | | | | | | | | | | | | -----------\ | | | | customer |-| | | | buy/sell | | | | | crypto | | | | |----------| | | | | | | GeneralBalanceUpdateCallback | | | ::onBalanceUpdate(GeneralBalanceUpdate) | | |<----------------------------------------------------| | | ------------------\ | | |-| update internal | | | | | balances data | | | | | structure | | | | |-----------------| | | | | | CustomerBalanceUpdateCallback | | | ::onBalanceUpdate(CustomerBalanceUpdate) | | |<------------------------------------------------------| | | | | */ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; enum Token { USD, EUR, BTC, ETH; } /** * Message/Event received from the upstream GeneralBalanceService, * for any change in the balance of any token for any customer * Balance Increases if getChange() > 0 * Balance Decreases if getChange() < 0 */ interface GeneralBalanceUpdate { long getCustomer(); Token getToken(); double getChange(); } /** * A callback provided to the upstream GeneralBalanceService, to be called * whenever there is a change in the balance of a customer for any token */ interface GeneralBalanceUpdateCallback { void onBalanceUpdate(GeneralBalanceUpdate update); } /** * Service to subscribe for updates in the balances of all customers * (provided) */ interface GeneralBalanceService { void subscribe(GeneralBalanceUpdateCallback callback); } /** * A callback provided to the CustomerBalanceService, to be called * whenever there is a change in the balance of a token for the subscribed customer */ interface CustomerBalanceUpdateCallback { void onBalanceUpdate(CustomerBalanceUpdate update); } /** * Message/Event sent to the downstream customer balance subscription, whenever * the balance for a token for that customer changes */ class CustomerBalanceUpdate { private final Token token; private double amount; private double change; public CustomerBalanceUpdate(Token token, double amount, double change) { this.token = token; this.amount = amount; this.change = change; } public Token getToken() { return token; } public double getAmount() { return amount; } public double getChange() { return change; } } /** * A Service used by customers to get notifications of changes * to their own specific balances, for each {@link Token} * (implement) */ class CustomerBalanceService { private Map customerBalances = new HashMap<>(); private Map> customerCallbacks = new HashMap<>(); // 1. Subscribe to balance changes for all customers by providing a GeneralBalanceUpdateCallback // to the GeneralBalanceService instance received on the constructor. public CustomerBalanceService(GeneralBalanceService balanceService) { GeneralBalanceUpdateCallback callback = new GeneralBalanceUpdateCallback() { @Override public void onBalanceUpdate(GeneralBalanceUpdate update) { updateBalance(update.getCustomer(), update.getToken(), update.getChange()); } }; balanceService.subscribe(callback); // TODO: subscribe to GeneralBalanceService notifications } // 2. Update internal data-structure to hold balances for each customer when the // GeneralBalanceUpdateCallback::onBalanceUpdate method is called. private void updateBalance(long customer, Token token, double change) { // customer exists in map List keys = customerBalances.keySet().stream().filter(x -> x.getId() == customer && x.getToken() == token) .collect(Collectors.toList()); double updatedBalance; if (!keys.isEmpty()) { double currentBalance = customerBalances.get(keys.get(0)); updatedBalance = getUpdatedBalance(currentBalance, change); customerBalances.put(keys.get(0), updatedBalance); } else { updatedBalance = getUpdatedBalance(0, change); CustomerBalanceKey key = new CustomerBalanceKey(); key.setId(customer); key.setToken(token); customerBalances.put(key, updatedBalance); } for(CustomerBalanceUpdateCallback callback : customerCallbacks.get(customer)) { CustomerBalanceUpdate update = new CustomerBalanceUpdate(token, updatedBalance, change); callback.onBalanceUpdate(update); } //customer does not exist in map // TODO: update in-memory data structure to reflect the received balance change } private double getUpdatedBalance(double currentBalance, double change) { return currentBalance + change; } // 3. If the customer has a registered callback provided via the CustomerBalanceService::subscribe // method, call the CustomerBalanceUpdateCallback::onBalanceUpdate method with an instance of // CustomerBalanceUpdate. public void subscribe(long customer, CustomerBalanceUpdateCallback callback) { customerCallbacks.computeIfAbsent(customer, (c) -> new ArrayList()).add(callback); if (customerCallbacks.containsKey(customer)) { customerCallbacks.get(customer).add(callback); } else { List callbacks = new ArrayList<>(); callbacks.add(callback); customerCallbacks.put(customer, callbacks); } // TODO: store the callback provided by the customer, so it can be triggered when a change is received } } class CustomerBalanceKey { private long id; private Token token; @Override public int hashCode() { return super.hashCode(); } public long getId() { return id; } public void setId(long id) { this.id = id; } public Token getToken() { return token; } public void setToken(Token token) { this.token = token; } }