Wallet Manager
The Task
Write code for a Wallet App that behaves as follows:
- User can deposit money into her wallet
- User can withdraw money from her wallet
- User can send money to another user
- User can check her wallet balance
Other:
- The Wallet App should keep track of all the users and wallets it contains
- No UI interface is required, solution is provided as reusable library code
- Submit your solution as a Zip file
Description
A library for depositing and withdrawing from a user's wallet, checking the
balance of a wallet and transferring funds between users. This library is
written in Go and requires a Postgres connection in order to retrieve Users
,
Wallets
and Transactions
from a database.
Usage
package main
import (
"context"
"log"
"github.com/jackc/pgx/v4"
"gitlab.com/mattshimwell/walletmanager"
)
const dummyUserID = "9843aa9a-61a4-41db-8622-9843aa9a4d4f"
const dummyDstUserID = "cfbbb1b8-8a28-4558-a1c6-cfbbb1b8d039"
func main() {
ctx := context.Background()
conn, err := pgx.Connect(ctx, "postgresql://root:root@localhost:5432/test")
if err != nil {
log.Fatalf("could not connect: %s", err.Error())
}
// Instantiate a new wallet manager
mgr := walletmanager.New(conn)
// List all wallets for a user
wals, err := mgr.List(ctx, dummyUserID)
if err != nil {
log.Fatalf("error listing user wallets: %s", err.Error())
}
wal := wals[0]
// Check a wallet's balance
bal, err := mgr.Balance(ctx, wal.ID)
if err != nil {
log.Fatalf("error retrieving balance: %s", err.Error())
}
log.Println(bal)
// Make a deposit
err = mgr.Deposit(ctx, wal.ID, float64(100))
if err != nil {
log.Fatalf("error making deposit: %s", err.Error())
}
// Make a withdrawal
err = mgr.Withdraw(ctx, wal.ID, float64(100))
if err != nil {
log.Fatalf("error making withdrawal: %s", err.Error())
}
// List all wallets for a user
newWals, err := mgr.List(ctx, dummyDstUserID)
if err != nil {
log.Fatalf("error listing user wallets: %s", err.Error())
}
newWal := newWals[0]
// Transfer from one wallet to another
err = mgr.Transfer(ctx, wal.ID, newWal.ID, float64(100))
if err != nil {
log.Fatalf("error making transfer: %s", err.Error())
}
}
Tests
A full suite of unit and integration tests are provided:
- To run unit tests (requires Go):
make race
.
- To run integration tests (requires Docker +
Compose):
make up && make integration
.
Test
coverage:
CI
GitLab CI has been added for this project, running Go tools and tests.
Decisions Made
- Given that the requirements state that this solution should be provided as
"reusable library code", no standalone API/RPC is included.
- I sought further clarification on what exactly was meant by "reusable
library code", and whether that meant that an API was not in scope for
this task. I was told that an API or standalone microservice was indeed
also acceptable. With more time allocated to this task I would therefore
add this in a further iteration (see improvements).
- Postgres was chosen as data store due to its relational nature, ease of setup
and familiarity. Replacing this data store with something else would be easy
enough considering the separation between the storage and service layers of
the library.
- The assumption is therefore made that the code implementing this library
will have access to a Postgres database.
- Given that the requirements state "The Wallet App should keep track of all
the users and wallets it contains" I think this is a fair assumption.
- As an alternative the library could provide a less
opinionated
database.Interface
which would then allow for other storage engines to be used.
- The act of depositing and withdrawing refer specifically to incrementing
and decrementing the
Balance
of a user's Wallet
. In a production setting I
would therefore expect this tool to either be integrated with or produce
events for a software component that would actually facilitate the moving of
money from one location/form to another.
- A
List
wallets by user method has also been added as an addition criteria
that was given, after requesting clarification on "The Wallet App should keep
track of all the users and wallets it contains".
- Migrations have been written for this database for the purpose of integration
testing.
Users
own one or more Wallet
, in which the current balance of each wallet
is stored.
Transactions
are separate entities that are owned by Wallets
and detail
the type of transaction made (credit
, debit
, transfer
) as well as the
amount and timestamp.
- In the case of (internal)
transfers
between wallets, both the source and
destination wallet ID are recorded in the corresponding Transaction
.
- Transfers between wallets are independent of the users that own them, so
functionally there is no difference between transferring between wallets
belonging to the same or different users. This means that the same method can
be used for transferring between users or between a single user's wallets.
Reviewing
- All efforts have been made to write and layout the code in as clean and
idiomatic style as possible, whilst taking care not to "over-optimise" given
the time allocated to complete the task.
- Interfaces are used to allow for effective mocking and testing, but are not
overused. The code follows "standard" idioms and structure,
per CodeReviewComments
and Effective Go.
- The
Manager
methods provided attempt to represent a DDD approach.
- To that end, the directory layout attempts to be as "vertical" as practical,
whilst still maintaining separations across files and packages.
- Dependency injection is handled sensibly, requiring the user to provide a
struct that satisfies
database.Interface
to walletmanager.Manager
, which
is in turn then used to instantiate the wallet and transaction stores.
- All exported functions are commented to ensure the code is well-documented.
- Lastly, the main
walletmanager.Manager
struct presents a clear and obvious
usage, ready to be included in whatever API/service/component this would
eventually be included in when imported into a production project.
Improvements
- Model
Users
in-code rather than just in the database and add some more
user-related methods. Currently they're only used when listing wallets.
- Modelling balances/transaction values as
float64
is probably overkill and
requires some Math
operations to address rounding errors. Depending on the
type of currency being dealt with, such values could be modelled as
simple int
.
- Allow for more CRUD operations on
Wallets
. Currently Wallets
cannot be
created or destroyed, only updated
Wallet.Balance
is the current source of truth for a current balance, and
should be exactly the sum of Transactions
for that user, however no such
reconciliation is ever performed. It probably should be in order to keep the
balance accurate
- Build out a gRPC server for this library so that it can stand alone (not in
scope)
- Containerise this gRPC server
- Add some deployment tooling (k8s, Helm, CD pipeline)
- Better handling of SQL errors (not found, duplicate, key constraint
violations, etc)
- To that end may be helpful to provide specific error types with custom
fields
- Add integration tests to CI
- Add a lot more fields on
Transactions
(status, more types, etc.)
Time Spent
~4 hours
Data Structure
Feedback
Overall this has been an interesting exercise, with plenty of opportunity to
demonstrate a variety of different programming disciplines. If I could offer
some feedback:
- It may be helpful to give some more context on the entities described in the
requirements. What's a User? What's a Wallet? When we say Deposit, what
do we mean?
Such Ubiqitous Language
makes it much easier to understand the task at hand.
- The requirements state that submissions should be provided as "library code",
but after having sought further clarification I was also advised that
providing an API or a standalone program were also acceptable. It may be
helpful to detail this in the requirements, and adjust the desired time spent
on the task accordingly.
- Further to that,
some Given-When-Then (or
alternative BDD format)
acceptance tests would be very useful in allowing a candidate to get right
into the task at hand, without worrying whether they've misunderstood what
they're supposed to be doing. We work with stories in tickets all day long, so
the closer such a task's description is to that format, the easier it is to
understand.
- If an API is indeed acceptable, perhaps some contract tests could be provided?