- Server has asset table, which has the symbols and names of the assets to be queried through API.
- Server queries the API for the assets, and generates a price table each 6 hours, and stores these price tables.
- When client becomes online, it fetches the price tables it missed from the server.
- Then, client creates snapshots for each 6 hours it missed (it can do so, since it has the price
tables now).
- say client was offline for 30 hours. when it is back online again, it will fetch the last 5 price tables,
- and there were no new transactions, since the client was offline,
- it will create 5 snapshots, using the current assets and 5 price tables.
- report will be generated on the client-side by utilizing the snapshots,
- Server does not store client's snapshots.
- Server only stores transactions, which can be processed on the client side to generate snapshots if needed.
- When client records new transactions, it also sends them to server, and server stores them.
- Client does not store transaction, because it does not need to. It is already storing snapshots, and transactions are only needed to generate snapshots.
- If client somehow loses snapshots (it could be that client switches to a new device, or offloads the app), client will simply query the server for fetching the transactions, and build the snapshots that it is currently missing.
- We will treat crypto assets differently
- stock and forex market have unique symbols, that's not the case for crypto
- for crypto, we need unique identifiers. In our case, they are coingecko ID's
- we display the symbol
BTC
to the user, but we query the API withbitcoin
- this mapping will be stored in
assets
document underserver
collection - client will fetch this information from server
- when client is displaying the name of the asset to the user, it will use the symbol (
BTC
) - all the communication between server-client will be done with the coingeckoID (
bitcoin
) - mapping one to another, is the responsibility of the client, because:
- client has enough information to perform this mapping
- and also, server does not need SYMBOL for api queries
- We cannot rely on client device's availability. So, the following should be done by the server:
- having an asset table that will be used to query the api's for price tables
- fetching price tables regularly
- storing the transaction log for each client
- since this will be an ever-growing data, we will be rotating yearly (firebase's limit is 1MB per doc)
- The data will be exposed to clients (for both read/write) only via cloud functions
- cloud functions are not subject to firestore rules, since they are run in a trusted server
- restricting every file in database for client access (for both read/write) is our approach
- fetching asset table from server, and storing it in local storage for the future
- fetching price tables from server, and storing them in local storage for the future
- sending a copy of the transactions (after compressing them) to the server, so that server can back up the client's history
- generating snapshots from the transactions
- generating reports from the snapshots
- knowing when will server have updates ready.
- in fact, server could notify the client on when will the next update be available,
- but that would require extra communication between server and client
- and, I want to minimize the costs for:
- firebase cloud function invocations
- server side computation
- so, although it is bringing some coupling, I've chosen to go with burdening the client instead
- deletion is not allowed for assets that are present in snapshots, since that would complicate a
lot of things
- when the user clicks on delete button, we will only make the amount 0
- the assets with
0 amount
, are hidden in the most of the UI for simplicity and minimalism
- if the asset is not present in any snapshots (say, it has been added accidentally just 2 seconds ago), deletion will actually delete it instead of making the amount 0
- we could disallow adding a new asset, if to be added asset is already present in the typeMap and categoryMap
- however, in this case, user will be confused. They will get
asset already exists
error, yet the asset might be hidden in UI- consider assets with 0 amount (hidden ones)
- that's why, we allow adding a new asset, even if it exists. Asset addition works in the following
way:
- if asset does not exist, simply add it
- if asset does exist:
- add the given amount to the previous amount
- compare the old category of the existing asset with the given new category
- if they are different, move the asset from old category to new one
- We are allowing custom categories, so we can't get away with a single map that only consists
of
assetType
-> (assetId
,assetData
) - we can somehow stick
category
information to the value part ofassetType
map, but then queries related tocategory
would be inefficient:- fetching all the assets under a specific category would require traversing the whole map. This is very inefficient
category
related queries happening a lot in the application (category pages, report generation)
- we can't have only a single
categoryMap
, and store theassetType
information in the value part as well:- due to,
assetType
related queries also happen frequently (price update for each day, etc.)
- due to,
- solution is, to have 2 separate maps:
categoryMap
andassetTypeMap
:- to reduce duplication:
assetData
will only be stored inassetTypeMap
categoryMap
will only store a tuple of (AssetType, AssetID)assetTypeMap
will store an inner mapping, where key is:AssetID
, and value isAsset
- where
Asset
is a struct, with fields:amount
,price
,category
- where
- this way, we can efficiently query
AssetID
s based on bothcategory
andassetType
- also, we can retrieve the
category
orassetType
information from both maps
- to reduce duplication:
- another solution might be using doubleMap (map with 2 keys), feel free to open a PR about that if you feel adventurous!
- say, the user bought 0.5 eth, and after 2 seconds, he sold 0.5 eth (effectively, he did nothing),
then he pressed
save all
in the edit assets page - when
save all
button is triggered, client will compress all the transactions (merge the transactions for the same asset under one), and send them to server like that - in fact, server should compress them a second time (because, the user might buy 0.5 eth,
hit
save all
, then sold 0.5 eth, and hitsave all
again in succession). But server is not doing that, the reasons are:- server needs to look at the previous transactions for that (and worst case, because of the rotation, server needs to parse 2 document's content)
- this means, 2 additional read cost per each transaction submit to the server by the client side
- also, this means extra CPU usage cost for the server
- the benefit is very minimal, doesn't worth the extra cost (both server costs, and developer costs)
- client will have an additional logic to compensate
- the additional cost to client is only more cpu usage, and happens only when client fetches transactions from the server (which should happen only once per fresh device)
- Adding retroactive transactions should not be possible. Because they introduce the following
complexities:
- if there isn't any snapshot for that date, have to create the snapshot, and add that asset
- now, when are doing comparison based on dates, what will happen to other snapshots where this asset might be missing?
- if there is already a snapshot with the given asset, should we accept this manual addition? Maybe user made a mistake, and it will override the actual/correct data.
- the API needs to change as well
- not a common use case, and this can be opening a can of worms. So, better to avoid it
- if there isn't any snapshot for that date, have to create the snapshot, and add that asset