{"id":17119095,"url":"https://github.com/victorges/nuledger","last_synced_at":"2025-03-24T02:19:15.398Z","repository":{"id":69945857,"uuid":"353186531","full_name":"victorges/nuledger","owner":"victorges","description":"Simple ledger and transaction authorizer application. Created to demonstrate software engineering skills and good practices.","archived":false,"fork":false,"pushed_at":"2021-05-08T17:17:09.000Z","size":243,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-01-29T08:23:45.658Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/victorges.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-03-31T01:17:23.000Z","updated_at":"2021-04-21T00:28:15.000Z","dependencies_parsed_at":"2023-02-27T16:00:45.701Z","dependency_job_id":null,"html_url":"https://github.com/victorges/nuledger","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/victorges%2Fnuledger","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/victorges%2Fnuledger/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/victorges%2Fnuledger/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/victorges%2Fnuledger/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/victorges","download_url":"https://codeload.github.com/victorges/nuledger/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245195962,"owners_count":20575938,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-14T17:56:16.722Z","updated_at":"2025-03-24T02:19:15.376Z","avatar_url":"https://github.com/victorges.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# nuledger\n\nThis is an implementation of a simple \"new\" ledger, originally for a remote\ninterview. It's original features included the tracking of state of a single\naccount, with an available limit of currency to be spent and whether its card\nwas active or not. Also multiple rules to authorize transactions in the account.\nThe active state of the card cannot be changed, but the available limit should\nreduce for every transaction that is authorized.\n\nImplemented the [specification](#Spec) with some design decisions to make it easy to\nexpand in the future. Be it with new business logic for authorizing transactions\nas well as incrementing the wire format in which the operations are received or\nthe actual ledger implementation.\n\nAfter submitting the project for evaluation, also implemented support for\nmultiple accounts, which I felt would be a nice addition. The next nice feature\nto add after that could be to support changing the active state of an account's\ncard, for example to (un)block the card of an existing account.\n\nThere are both extensive documentation and 100% coverage tests of the code,\nwhich can be both inspected easily in the browser. The integrated server for\ndocumentation (`godoc`) can be started with `make doc` while the tests server\n(`goconvey`) with `make test_server`.\n\n## Project Operation\n\nThere is a `Makefile` with hopefully all the commands that will be necessary for\ninspecting, building, testing and running the project.\n\n### Requirements\n\n `go version 1.13+` and/or `docker`\n\n### Inspect\n\nInstead of reading the documentation directly in the source files, you can read\nit in the `godoc` interface in your browswer.\n\nTo start the `godoc` server:\n```\nmake doc\n```\n\nThen read the project documentation in your browser at:\nhttp://localhost:6060/pkg/nuledger\n\n### Test\n\nTo generate the mock implementations:\n```\nmake generate\n```\n\nNotice it is not always necessary to re-generate the mocks, only when the mocked\ninterfaces have changed and thus need some update. Otherwise, they're already\nthere and checked-in to version control.\n\nTo execute the tests:\n```\nmake test\n```\n\nAlternatively, start the `goconvey` server for visualizing the test results:\n```\nmake test_server\n```\n\nThen watch the test results display in your browser. If the browser doesn't open\nautomatically, click:\nhttp://localhost:8080\n\n### Build\nTo build the project locally:\n```\nmake build\n```\n\nIt outpts the executable file to the `./build` folder with `authorizer` name.\ne.g. You can test it with one of the integraction test cases with:\n```\nmake build \u0026\u0026 ./build/authorizer \u003c testcases/base/in.jsonl\n```\n\nAlternatively, to build the project with Docker (also runs tests):\n```\nmake docker\n```\n\n### Run\nTo run the application locally:\n```\nmake run\n```\nThat one doesn't generate build output as well so it leaves the folder clean.\n\nTo run the application in Docker:\n```\nmake docker_run\n```\n\nThat will run the docker build command again and then run the container. You can\npipe the input directly to make and it will work as expected, e.g.:\n```\nmake docker_run \u003c testcases/highFreqTransactions/in.jsonl\n```\n\n## Spec\n\nThe application should read all the input from `stdin` and write all the output\nto `stdout`, both in the [JSON Lines](https://jsonlines.org) format. Each JSON\nin the input represents an operation to be performed, either creating an account\n(`account` field in input object) or performing – after authorizing – a\ntransaction (`transaction` field).\n\nEach output should include the final state of the corresponding account, in the\n`account` field of the output object. It could be either the transformed state\nafter the given operation was performed or the exactly the previous state in\ncase there was some business rule violation for performing the transaction.\n\nThe business rule violations are well-specified, being:\n - `account-already-initialized`: Account had already been initialized when\n   another create account operation was requested (with the same account ID).\n - `account-not-initialized`: A perform transaction operation was attempted\n   before the corresponding account was actually initialized.\n - `card-not-active`: A perform transaction operation was attempted in an\n   account whose card is not active.\n - `insufficient-limit`: A transaction is attempted with an amount higher than\n   the available limit of the account.\n - `high-frequency-small-interval`: Too many transactions are performed in the\n   same account within a small interval. This limit is currently configured as a\n   maximum of 3 transactions in the same account every 2 minutes.\n - `double-transaction`: A duplicate transaction was attempted. This means that\n   the attempted transaction has the same account, amount and merchant of a\n   recent transaction. A transaction is currently considered to be recent if\n   performed at most 2 minutes ago.\n\nAny violations of business logic must be included in the `violations` field of\nthe output object written to `stdout`. This means that if the operation was not\nauthorized then at least one violation will be present in the output.\n\nSome examples of input/output combinations can be seen in the `testcases` folder\nin the root of the project (run by the `main_test.go` test). A very simple one\n(`base`) is:\n\n - Input:\n```\n{\"account\": {\"active-card\": true, \"available-limit\": 100}}\n{\"transaction\": {\"merchant\": \"Pizza Zagga\", \"amount\": 20, \"time\": \"2019-02-13T10:00:00.000Z\"}}\n{\"transaction\": {\"merchant\": \"TT Burger\", \"amount\": 90, \"time\": \"2019-02-13T11:00:00.000Z\"}}\n```\n - Output:\n```\n{\"account\":{\"active-card\":true,\"available-limit\":100},\"violations\":[]}\n{\"account\":{\"active-card\":true,\"available-limit\":80},\"violations\":[]}\n{\"account\":{\"active-card\":true,\"available-limit\":80},\"violations\":[\"insufficient-limit\"]}\n```\n\nNotice that every transaction has a timestamp, and it is a hard requirement by\nthe program that the timestamps must be received in order. Otherwise, we\nwouldn't be able to process transactions one by one since many of the algorithms\ndepend on the chronological order. If the program didn't have that guarantee,\nit'd also need to keep a buffer of some of the last seen transactions to be sure\nto process each transaction only when no transaction before it could show up.\n\nAlso notice that this example does not have any reference to an account ID.\nSince the multi-account was implemented as an additional feature, it is also\ncompletely optional. So an account can specify no ID which has the same behavior\nas an empty string ID. To use the multi-account feature, an `id` field has to be\nspecified in the create account operation and an `accountId` field has to be\nspecified in the perform transaction operation, and it will correspondingly\nappear in the output objects.\n\n## Design\n\nSome design decisions were made, so some of the higher level ones will be\ndetailed here for easier understanding of the whole project. As mentioned in the\nprevious session, lower-level documentation is also available for all the\nexported components in the code and can be inspected either directly in the code\nor in the browser via `godoc`.\n\n### I/O Processor\n\nFor the input/output processing part of the application I created a separate\n`iop` package, which stands for exactly that.\n\nThe application was proposed with a very specific form of input/output, reading\nline-separated JSONs from the standard input, processing them and writing them\nto the standard output. With that in mind, I believed it made a lot of sense to\nseparate that specific logic, both this read-process-write pipeline and the\nactual JSON format of the received and returned objects, into that separate\npackage.\n\nThe package also exports a single interface which is where any business logic\nfor processing the operations needs to plugin. The interface basically receives\nthe input JSON and returns the response JSON, so a slightly higher level\ncomponent can already be created without worrying about the I/O pipeline.\n\nThe `iop` is similar to an HTTP framework that handles the lower level protocol\nand provides the higher level objects to a component to do any business logic.\n\n### Rule Authorizers\n\nThe other core piece of the code architecture are the rule authorizers. Their\ninterface and some generic helpers are defined in the `authorizer/rule` package,\nwhile the specific rule implementations are in `authorizer/rules`.\n\nThey exist so that the actual account-managing part of the application are as\nextensible as possible, with authorization rules being easily created or removed\nfrom the default set of rules. In the specific implementation, each violation\ncode is validated by a specific authorizer, but we can also combine multiple\nof them in a single authorizer if helpful. The violations are returned as errors\nin the authorization, later translated into an actual violations array in the\nresponse.\n\nThese can also allow for flexible managing of accounts, and we could choose\ndifferent sets of authorizers depending on other specific rules. For example, an\naccount could have some overdraft feature to alow it to go below its limit, so\nwe could include the specific authorizer about that or not.\n\nFor the specific violation about maximum frequency of transactions, there is a\nrate limiter utility in the `util` package which has the core frequency limiting\nlogic. It implements an \"optimal response\" algorithm for rate-limiting, in the\nsense that it might become expensive for a lot of events but it guarantees that\nthe correct response will be given regarding the number of transactions in the\npast observation interval. Decided to use it for the double transaction as well\nto avoid re-implementing some custom logic, even though the double transactions\nwould be rather simpler to implement directly with just a timestamp.\n\n### Authorizer\n\nThe `authorizer` is the package with the \"most core\" business logic of the\napplication apart from the rule authorizers mentioned above.\n\nThe first relevant component is an implementation of an I/O handler which\nreceives the JSON objects from the `iop` package, processes them internally and\ntranslates the response back to the I/O pipeline. Following the same analogy of\nHTTP frameworks, it would be an API router/controller which routes specific\nrequests to the corresponding API that should be called and then translates the\nresponse to the protocol being used.\n\nThe final one is the ledger itself, which ends up being pretty simple given the\nabove abstractions. It provides explicit methods for each of the operations\nsupported by the system (creating an account and performing a transaction) and\nis the component called by the handler from the paragraph above. Its\nimplementation is rather simple though, since it only needs to validate the\n(non-)existence of the account, authorizer the operation with the configured\nrule authorizer, and then actually perform the operation if all is fine.\n\n### Tests\n\nThere are both integration and unit tests in the project.\n\nThe integration test is written in the root of the project, in the\n`main_test.go` file. It goes through all the test cases in the `testcases`\nfolder, each represented by a sub-folder with an `in.jsonl` and `out.jsonl`\nfiles for input and expected output respectively.\n\nThe unit tests are written across the project in the `*_test.go` files, at least\none in each (non-generated) package. These unit tests also make use of test\nmocks generated by the `golang/mock` library. The mock generation makes use of\nthe `go generate` command that processes a couple of `//go:generate` directives\nin some files, which leverage from the `gen_mocks.sh` script in the project root\nas well to save some boilerplate.\n\nAll the tests are also written with the `goconvey` library, so one can run their\nCLI/webserver to see a friendlier UI with all the executed tests, test coverage\nand which automatically refreshes with changes in the source code as well.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvictorges%2Fnuledger","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvictorges%2Fnuledger","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvictorges%2Fnuledger/lists"}