- Single API service designed to wrap all transaction behaviors.
- Wrapped in a docker image to ensure consistent builds across assessor machines.
- Directory structure:
api
: Root directoryports
: Entry points to system functionality, contain http handlers.core
: Shared functionality which could be used across multiple endpointsbank
: Wraps calls to the bank API (mock) and associated functionality.db
: Database interaction functionality.domain
: Shared class definitions which are used widely across codebase.transaction
: Core transaction functionality, i.e. business logic.
The entry points into system functionality. In this case only http handlers but enables clean addition of additional connection methods. I consider handlers to be the interface between the user interface and internal system functionality. As such no functional requirements should be met by code in ports. All it should do it extract relevant data from the user request, pass it to internal functions and parse the result to the correct response format and return it to the user.
The Bank API mock.
Went with simple mock functions (as opposed to an additional API service) for implementation and testing simplicity.
Does not support switching bank implementations/interfaces during run-time.
Considering this API as a microservice providing transaction operations I prefer to transfer incoming traffic to a new instance using a different implementation then to take on the implementation complexity of supporting run-time implementation switching without transaction loss.
(This issue was primarily considered based on a possible future implementation of asynchronous transactions.)
Implementing using a Go
interface
object to enable the (theoretical future) provision of a real implementation and a naive, in-memory mock for testing support.
The relational database functionality, containing functionality to create and interact with the transactions
table.
Similar to the design approach for ports
, db
is the interface between internal system and the database, enabling looser coupling of the system's transaction model and the database's transaction model.
Provides an in memory version to enable easier integration & end-to-end testing.
Common class implementations and isolated methods required across the codebase.
Whilst domain packages risk recreating the issues of a global util
packages if allowed to bloat.
I still like using them as the ability to prevent cyclical imports and encourage the use of common objects (as opposed to multiple fiat implementations in different areas that can arise in large project) is helpful.
The core internal functionality which implement the specification requirements, i.e. where business logic is implemented.
For persistent storage went with a standard SQL relational database for simplicity. Whilst for a proper implementation I would use a separate Postgres instance, for the purposes of this demonstration work only running a SQLite3 instance within the same docker image was deemed okay.
- A single database.
- Consistent card number & cvv formats.
- Only supporting a defined set of currencies (GBP, USD & EUR).
Note: Requires docker
To build the docker image wrapping the API run:
docker build -t processout .
Note: This may take a minute as cgo
is required to build the SQLite3 relational database package.
To run the API in a docker container from the image built above, run:
docker run -p 8080:8080 processout
This API is exposed on port 8080 and has two valid endpoints.
Makes a synchronous transaction request to an acquiring bank. Requires a JSON body in the format:
{
"timestamp_unix": 1726141174,
"card_no": "1234-5678-1234-5667",
"expiry_month": 5,
"expiry_year": 2026,
"cvv": "344",
"currency": "GPB",
"amount": 1234
}
It returns the transaction UUID and outcome in the following JSON:
{
"id": "de1c9648-8bd2-4b4e-b062-47160ccd6618",
"state": "Successful"
}
Returns the masked transaction details for a single transaction Returns the transaction details in following JSON
{
"id": "de1c9648-8bd2-4b4e-b062-47160ccd6618",
"timestamp_unix": 1726141174,
"card_no": "****-****-****-5667",
"expiry_month": 0,
"expiry_year": 0,
"cvv": "***",
"currency": "GPB",
"amount": 1234,
"state": "Successful"
}
- Consider representing card numbers as
[][]int8
and cvvs as[]int8
for optimization over using[]string
andstring
. - Running a separate Postgres instances for proper persist storage.
- Implementing asynchronous transactions, only requires implementing the relevant bank mock, and accept additional load by having user's poll the
/transaction/:id
endpoint for state updates. - Caching for
/transaction/:id
, though if supporting asynchronous transactions would require providing a non-cached endpoint for 'live' transactions.