wallet

package module
v0.0.0-...-9d4beaf Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 29, 2019 License: MIT Imports: 9 Imported by: 0

README

Table of Contents

Take Home Test definition

We make extensive use of domain‒driven design (DDD) with Go kit when developing in Go.  For the task we ask to implement generic Wallet service with a RESTful API.  For extra context and perspective assume it will be used as a core service in your fintech startup.

User Stories

  • I want to be able to send a payment from one account to another (same currency)
  • I want to be able to see all payments
  • I want to be able to see available accounts

Assumptions and requirements

  • Only payments within the same currency are supported (no exchanges)
  • There are no users in the system (no auth)
  • Balance can't go below zero
  • More than one instance of the application can be launched

It's okay to make other assumptions as long as you are explicit about them. Feel free to ask further questions/clarifications once you have those.

Example entries

Account entity:

id: bob
balance: 100.
currency: USD
---
id: alice
balance: 0.
currency: USD

Payment entity:

account: bob
amount: 100.
to_account: alice
direction: outgoing
---
account: alice
amount: 100.
from_account: bob
direction: incoming

Evaluation

Our goal with this test project is to see your  best/most idiomatic code in Go.  Attention will be put on but not limited by the listed aspects:

Docs:

  • Code documentation
  • API docs (ideally in markdown format, e.g., docs/api.md)
  • Human oriented README explaining your project's purpose, how to set it up, run tests,
  • code linting, start contributing
  • Doc strings which should explain "real world" problem you're solving, attributes, params documenting

Architecture and Design:

  • Simplicity
  • Expected design patterns, decoupling, complexity isolation
  • DDD (Domain Driven Design)

Code:

  • Hosted on any public/private git storage (github/gitlab/bitbucket)
  • Human‒oriented with recomended conventions (Effective Go, naming in Go, Go for Industrial Programming, Practical Go),  descriptive names of variables, classes, functions, apps, packages,  repository itself no "dead code" inside a repository (e.g., empty modules, unused settings, excessive blank lines, etc)
  • Idiomatic (data model and structures, best practices)
  • Proven to work (covered by tests)
  • Implementation using  go‒kit (if you write in Go) 

Infrastructure:

  • Deployability
  • External dependencies (if any) choice justification
  • PostgreSQL as storage engine (be aware of DB transactions, locks, race conditions)

Bonus points:

  • Deployment with Docker/Docker‒compose

Timeframe

There is no strict deadline but we would like to get ETA from you to finish the project upon reading the document and understanding it.


Comments From Developer

Decisions reasoning

  • Postgres has Money column type, but I used Numeric(12, 2) because of Jeff Bezos has $100B and https://www.postgresql.org/message-id/flat/20130328092819.237c0106@imp#20130328092819.237c0106@imp
  • Golang has math/big.Float type, but it has very unfriendly API for business app. I rejected github.com/Rhymond/go-money because it doesn't support Cents. I stopped on github.com/shopspring/decimal but wrapped into Domain object wallet.Money. But few other implementations with bunch of tests are available: https://github.com/cockroachdb/apd or https://github.com/ericlagergren/decimal
  • In DDD we can separate Domain structures from Persistence structures. But I will avoid it to reduce complexity. Not perfect, because Domain structures can be "not-flat" as Postgres expecting. And if we will follow Postgres limitations then it will Database Driven Design :-) But It's ok for given project.
  • Restful API must return only resource objects, it means no additional nesting: /accounts must return [...] instead of {"data":[...]} And I will follow this design. But nobody does, because HTTP headers aren't developers friendly and because real API's wish to return much more info than just resource:

Project structure reasoning

  • DDD was required by task creator.
    • I used Golang adopted DDD packages structure: root package for Domain objects and no deep nesting of sub-packages (all in 1-level depth), you can get more reasoning in article of Ben Johnson where he shows examples from go std lib.
    • account and payment packages are follow go-kit DDD example and have separation of logging/instrumenting/transport logic from business-logic.
    • postgres for implementation of Repositories based on Postgres.
    • Single mock package to store all mocks (generated by ./mock_gen.go)
  • walletctl package providing single binary for HTTP Server and CLI commands. I didn't use any special package for cli, to save time. go run ./walletctl to get help.

REST design reasoning

  • To make POST requests idemponent user must supply Idempotency-Key: UUID() header. Server will response 409 code if such key already exists.
  • idempotency_keys table doesn't

Command to start

make start

Deployment

  • I didn't use https://docs.docker.com/compose/compose-file/#depends_on for app definition in docker-compose.yml. Because in microservice world we must be protected against cascade down. If DB or some underline service is down - there is no reason for and App to be down - it still can serve things from cache or other services. I didn't put much effort into "graceful degradation" here, but it's nice to have.

Optional future ToDo

  • Hide payment_id to hide amount of company clients: 1
  • Add hystrix circuitbreaker
  • Release by goreleaser, and publish image to dockerhub
  • Make DB name more configurable - to parallelize integration tests and do better cleanup. Don't truncate tables on tests startup.

Documentation

Overview

Package domain contains the heart of the domain model.

Package contains the heart of the domain model.

Index

Constants

This section is empty.

Variables

View Source
var ErrIdempotencyKeyExists = errors.New("such Idempotency Key already exists")
View Source
var ErrInsufficientFunds = errors.New("insufficient funds")
View Source
var ErrNotFound = errors.New("not found")

Functions

This section is empty.

Types

type Account

type Account struct {
	ID        AccountID
	CreatedAt time.Time
	Balance   Money
	Currency  Currency
}

Account is the central class in the domain model.

type AccountID

type AccountID string

AccountID uniquely identifies

type AccountRepository

type AccountRepository interface {
	// Store create or update given account
	Store(ctx context.Context, account *Account) error
	// Find fetching single account
	Find(ctx context.Context, id AccountID) (*Account, error)
	// FindAll fetching all accounts
	FindAll(ctx context.Context) ([]Account, error)
	// AddMoney makes given account more rich
	AddMoney(ctx context.Context, id AccountID, amount Money) error
	// SubtractMoney makes given account more poor
	SubtractMoney(ctx context.Context, id AccountID, amount Money) error

	// On create new instance of Repository on given db connection or transaction
	On(db orm.DB) AccountRepository
}

type Currency

type Currency struct {
	currency.Unit
}

func NewCurrency

func NewCurrency(v currency.Unit) Currency

func (*Currency) Scan

func (u *Currency) Scan(value interface{}) error

func (Currency) Value

func (u Currency) Value() (driver.Value, error)

type Money

type Money struct {
	decimal.Decimal
}

Money base datatype to store money. Underline using immutable arbitrary precision fixed-point decimal.

func NewMoneyFromFloat

func NewMoneyFromFloat(v float64) Money

NewMoneyFromFloat use this method to create new hardcoded money value

type Payment

type Payment struct {
	ID          PaymentID
	CreatedAt   time.Time
	Amount      Money
	Direction   PaymentDirection `pg:"type:payment_directions"`
	Account     AccountID
	FromAccount AccountID
	ToAccount   AccountID
}

Payment is the central class in the domain model.

type PaymentDirection

type PaymentDirection string
const (
	PaymentIncoming  PaymentDirection = "incoming"
	PaymentOutcoming PaymentDirection = "outgoing"
)

type PaymentID

type PaymentID string

PaymentID uniquely identifies a particular payment.

type PaymentRepository

type PaymentRepository interface {
	// Store create or update given payment
	Store(ctx context.Context, payment *Payment) error
	// Find fetching single payment
	Find(ctx context.Context, id PaymentID) (*Payment, error)
	// FindAll fetching all payments
	FindAll(ctx context.Context) ([]Payment, error)

	// RegisterIdempotencyKey key which prevent duplicate writes. Client can't send us more than 1 request with same key.
	RegisterIdempotencyKey(ctx context.Context, idempotencyKey uuid.UUID) error
	// RemoveIdempotencyKey just removing key.
	RemoveIdempotencyKey(ctx context.Context, idempotencyKey uuid.UUID) error

	// On create new instance of Repository on given db connection or transaction
	On(db orm.DB) PaymentRepository
}

Directories

Path Synopsis
Package account provides the use-case of accounts management.
Package account provides the use-case of accounts management.
Package mock is a generated GoMock package.
Package mock is a generated GoMock package.
Package payment provides the use-case of payments management.
Package payment provides the use-case of payments management.
api

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL