Skip to content

DDD/Event-Sourcing/CQRS-based distributed bank-account transactions example using Go

License

Notifications You must be signed in to change notification settings

Jaskaranbir/es-bank-account

Repository files navigation


Introduction

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.

Use-case

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.

Run info

Running application

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.

Running tests

Ginkgo-CLI:

ginkgo -r \
      --p \
      --v \
      --race \
      --trace \
      --progress \
      --failOnPending \
      --randomizeSuites \
      --randomizeAllSpecs

Go-tests:

go test -v ./...

Architecture

Even though this is a single application, the design is similar to how a micro-service architecture would be implemented (using Go-routines).

Bus

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.

Components

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 for Creator after receiving TxnRead event from Reader.

  • Runner: Handles lifecycly of above routines.

Application Flow

image

* AccountLimitsExceeded refers to exceeding number of transactions or load-amounts from daily/weekly-limits, or withdrawing with insufficient-funds (check Use-case).

Event-Sourcing implementations

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.

Logging

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.

Error Handling

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:

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.

Testing

The principles of Blackbox-testing are used. We use Ginkgo and Gomega for BDD-testing.

About

DDD/Event-Sourcing/CQRS-based distributed bank-account transactions example using Go

Topics

Resources

License

Stars

Watchers

Forks

Languages