This is an example of developing a bank-account like transactional-system using domain-driven-design, event-sourcing, and CQRS.
This doesn't require any additional setups/configs and will work out of box. Below are some more details about architecture and design of this application.
The use-case is to develop a program that accepts or declines attempts to load/withdraw funds into customers' accounts.
Each customer is subject to various daily and weekly limits, such as:
- Maximum number of transactions in a day or week
- Maximum amount of funds loadable into an account in a day or week
These limits are currently configured using the config.
go run main.go
This will read transactions from input.txt
(from project-root), and generate an output.txt
with results. Sample input.txt and output.txt are provided.
Ginkgo-CLI:
ginkgo -r \
--p \
--v \
--race \
--trace \
--progress \
--failOnPending \
--randomizeSuites \
--randomizeAllSpecs
Go-tests:
go test -v ./...
Even though this is a single application, the design is similar to how a micro-service architecture would be implemented (using Go-routines).
Since the design is based on Event-Sourcing, a Bus is used to deliver messages (commands/events) across modules.
This Bus is really just performing fan-in and fan-out techniques using Go-channels, and uses concept of topics (called Actions
in context of our application) like Kafka or other message-brokers out there.
Following are the major components in the system:
-
Reader: Simulates our input-request (which would usually be sent via a REST/GraphQL-call). For now, the requests are read from an IOReader interface line-by-line (which by default is a file), and the event
TxnRead
is published on EventBus as each line is read. -
Creator: Validates the data-read by
Reader
and creates a transaction-request using that data. -
Account: Processes the transaction-requests, which includes depositing/withdrawing funds and validating transactions (such as checking for duplicate transactions, or checking that transaction doesn't exceed daily/weekly account-limits).
-
AccountView: Stores the results of transaction-processed by account in a report-like format.
-
Writer: Writes the provided data to an IOWriter interface (which by default is a file).
-
ProcessManager: Handles coordinating between above routines. For example, this creates the
CreateTxn
command forCreator
after receivingTxnRead
event fromReader
. -
Runner: Handles lifecycly of above routines.
* AccountLimitsExceeded refers to exceeding number of transactions or load-amounts from daily/weekly-limits, or withdrawing with insufficient-funds (check Use-case).
Various components and utilities required for Event-Sourcing (such as EventStore and message-Bus) have been implemented using in-memory storage. These are part of the eventutil package.
Contextful logging has been one of the key aspects, and achieving it through concurrent flows and multiple modules can be tricky.
So we use following logging-design:
-
Every module gets its own logger instance. The instance prefixes all logs from that module by module's name.
-
Each operation can add its own logging prefixes (or contexts), and any further logs from that operation will use that prefix.
-
Logging-levels can be specified for all modules at once, or for each individual module using env-vars (check StdLogger for more details).
This provides with some extensive logs which allows tracing through application easily. Here's a sample log-file with trace
-level logs for a single transaction flow.
With extensive concurrent-flows through channels, propagating errors and controlling application-flow can be tricky.
There are two main packages to allow efficient error-handling/propagation:
-
github.com/pkg/errors: Allows wrapping errors to add contextual-information.
-
golang.org/x/sync/errgroup: Provides a clean interface to propagate errors from Go-routines.
Here's a sample error log-line displaying a mocked error in AccountView-hydration:
2021/01/23 12:02:55 error running domain-routines: Some routines returned with errors:
[txnResultView]: transaction-result-view returned with error: error in account-view routine: listener-routine exited with error: error hydrating transaction-result view: some mock critical error
Notice that since this mocked-error was a critical-error, this caused the application to exit fatally.
Controlling application-flow on critical-errors is handled by Runner and ProcessManager.
The principles of Blackbox-testing are used. We use Ginkgo and Gomega for BDD-testing.