{"id":35439120,"url":"https://github.com/rieske/event-sourced-account","last_synced_at":"2026-06-01T23:00:38.098Z","repository":{"id":36963253,"uuid":"195552115","full_name":"rieske/event-sourced-account","owner":"rieske","description":"Frameworkless event-sourced account implementation","archived":false,"fork":false,"pushed_at":"2026-05-27T22:10:11.000Z","size":3074,"stargazers_count":6,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-05-28T00:22:39.716Z","etag":null,"topics":["event-sourcing","frameworkless"],"latest_commit_sha":null,"homepage":"","language":"Java","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/rieske.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,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2019-07-06T15:02:23.000Z","updated_at":"2026-05-27T22:07:31.000Z","dependencies_parsed_at":"2026-02-20T15:15:01.756Z","dependency_job_id":null,"html_url":"https://github.com/rieske/event-sourced-account","commit_stats":null,"previous_names":[],"tags_count":1215,"template":false,"template_full_name":null,"purl":"pkg:github/rieske/event-sourced-account","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rieske%2Fevent-sourced-account","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rieske%2Fevent-sourced-account/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rieske%2Fevent-sourced-account/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rieske%2Fevent-sourced-account/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rieske","download_url":"https://codeload.github.com/rieske/event-sourced-account/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rieske%2Fevent-sourced-account/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33797128,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-01T02:00:06.963Z","response_time":115,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["event-sourcing","frameworkless"],"created_at":"2026-01-02T23:23:02.451Z","updated_at":"2026-06-01T23:00:38.078Z","avatar_url":"https://github.com/rieske.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"## Event Sourced Account\n\n[![Actions Status](https://github.com/rieske/event-sourced-account/workflows/build/badge.svg)](https://github.com/rieske/event-sourced-account/actions)\n[![Maintainability](https://qlty.sh/gh/rieske/projects/event-sourced-account/maintainability.svg)](https://qlty.sh/gh/rieske/projects/event-sourced-account)\n[![Code Coverage](https://qlty.sh/gh/rieske/projects/event-sourced-account/coverage.svg)](https://qlty.sh/gh/rieske/projects/event-sourced-account)\n\nA lightweight, frameworkless event sourced Account implementation.\n\n\n### Implementation\n\nThe purpose of this project was for myself to better understand the complexities of event sourcing\nand apply the lessons learned from hype-driven event sourcing implementations gone wrong.\n\nThe service is test-driven bottom-up: domain -\u003e event sourcing -\u003e public API.\n\n#### Domain\nFour operations can be performed on an account - open, deposit, withdraw, and close.\nDomain captures the business rules of when an account can be interacted with, what amounts\ncan be deposited and withdrawn and under what circumstances.\nMoney transfer between accounts is a withdrawal from one account and deposit to\nanother that has to happen within a transaction - either both operations succeed or none.\n\n#### Event sourcing\nIn essence, event sourcing is a data storage technique when the state is stored as a sequence of\ndomain events as opposed to storing the final state.\nIt comes with some beneficial side effects, most prominently pronounced ones being:\n - the \"free\" audit log recorded by the events.\nIn reality, nothing is free, and the cost here is complexity, especially when the software evolves, and the amount of stored data grows.\nYes, hard drives are cheap, and you can store insane amounts of data.\nIt is managing and working with the insane amounts of continuously growing data that becomes complex.\n - the ability to build different read-optimized projections of the data for different use cases using a pattern like CQRS.\n - potential to emit certain external events for other systems to react to.\nBeware: never expose/publish the internal domain events used for event sourcing to an external system.\nDo not make the internal events your public API.\nIf you do, all your domain implementation details and the persistence layer will become your public contract.\nThis is the same evil as sharing a regular flat database schema between multiple services.\n\nIn our case, the most important aspect of event sourcing is the optimistic locking that it enables that can help to\nensure consistency of the data in a distributed environment, enabling horizontal scalability of the service.\n\n#### Concurrency\nThe event store here is key to ensuring consistency in a multithreaded environment.\nSpecifically, the constraints that the database provides - remove the (aggregateId, sequenceNumber)\nprimary key and all but consistency tests will pass.\n\nThe trick is to read the state of an account first, compute the events to be applied and insert them within a single transaction.\nUpon reading the current state, we know the sequenceNumber of the last event. It gets incremented by one for each subsequent event\nthat we plan to insert. If some other client inserts an event first, then the constraint will reject our transaction, and we will have\nto re-read the current state and retry.\n\nService behavior in a distributed environment is tested at multiple layers:\n- an in-memory event store, H2, and a real database container when multiple threads hammer a single account\n- multiple clients hammering an account via a load balancer through two instances of the service connecting to a database\n\n#### Idempotency\nWhat can be important when dealing with money and especially when requests come over an\nunreliable network (and the network is unreliable by definition) is idempotency. When a client\nrequest gets interrupted due to whatever reason, the client might not know whether the\nrequest was handled or not and might retry. This might result in a double transfer, double\ndeposit or withdrawal had the original request been handled successfully. To prevent such\ncases, the client should supply a unique transaction id (a UUID in our case) for each\ndistinct operation. This id is persisted, and in case a duplicate request comes in, it will\nbe accepted, but no action will be taken since we know we already handled it.\nTransaction ids can not be reused. The current implementation is a bit naive as it does not take into account\nthe type of operation in the context of idempotency, just the transaction id together with\naffected account id, meaning that given a transaction id that was used for a deposit\nwould be used for a withdrawal, the service would respond that it accepted the request.\nMaybe a better way would be to conflict in such cases.\n\n\n### API\n\n- open account: `POST /api/account/{accountId}?owner={ownerId}` should respond with `201`\n  and a `Location` header pointing to the created resource if successful\n- get account's current state: `GET /api/account/{accountId}` should respond with `200`\n  and a json body if account is found, otherwise `404`\n- deposit: `PUT /api/account/{accountId}?deposit={amount}\u0026transactionId={uuid}`\n  should respond with `204` if successful\n- withdraw: `PUT /api/account/{accountId}?withdraw={amount}\u0026transactionId={uuid}`\n  should respond with `204` if successful\n- transfer: `PUT /api/account/{accountId}?transfer={targetAccountId}\u0026amount={amount}\u0026transactionId={uuid}`\n  should respond with `204` if successful\n- close account: `DELETE /api/account/{accountId}` should respond with `204` if successful\n\n\n### Tests\n\nTests are separated in their own source sets and given their own Gradle task by test category.\nTests in the `test` source set and executed by `test` task are the fast unit tests.\n\nThe next level are the integration tests that use both Postgres and Mysql backed event store implementations.\nThey use testcontainers to spawn real database instances. Integration tests live in `integrationTest` source\nset and are executed using `integrationTest` task.\n\nFinally, a couple of end-to-end tests that focus mainly on sanity testing consistency in a distributed\nenvironment. `blackBoxTest` source set and a task with the same name.\n\nSince I was test driving this service from the domain up to the event sourcing infrastructure and lastly\nup to the API, some of the tests might be redundant and functionality might be tested several times.\nWith the in-memory event store and h2 event store, the unit tests exercise the whole module really fast\n, and it might even be possible to move the remaining lower-level tests higher up. This would allow\nus to test the functionality solely via the API and not have any implementation details tests.\nWhich can, in turn, make changes to internal implementation details easier to make with the absence of\nimplementation-oriented tests. I did not use any mocking framework to avoid testing implementation\nand focus solely on the functionality.\n\n\n### Potential red flags\n\n- I used longs for monetary amounts, assuming those are in minor units/cents. I am aware that money\nis a delicate matter, and extra care is needed when dealing with it in software. Since\nthis service performs only basic addition and subtraction, I decided to use cents for now\nand focus on other things. Should I need to deal with floating points, I would at the very\nleast go for BigDecimal and probably do some investigation around current best practices -\nI know there is a javamoney implementation, also an older joda money one.\n\n- I did not take currency into account - all accounts are the same currency for now. Should I\nneed to add currency, I'd probably have to refine the type that holds amounts to include\nthe currency, prevent deposits/withdrawals if the account currency does not match. This would\nprevent cross-currency transfers right away. Currency conversion for cross currency transfers\nis something I'd have to figure out. This potentially could be out of scope for accounts\nservice itself.\n\n- Exception types in the domain - probably would make sense to create specific types for\nInsufficientBalance, AccountClosed exceptions etc. Kept it simple with IllegalArgument/State\nexceptions for the sake of avoiding unneeded class count explosion.\n\n### Building\n\n```shell script\n./gradlew test\n```\n\nThe test task will only run the fast unit tests (including event store tests with H2 in postgres mode).\n\nIn order to run the same set of tests targeting Postgres and Mysql, run\n```shell script\n./gradlew integrationTest\n```\nThose will be much slower - they spawn the actual Postgres and Mysql instances using testcontainers and thus\nrequire a running docker daemon.\n\nAnd another round of slow tests that test for consistency in a distributed environment:\n```shell script\n./gradlew blackBoxTest\n```\nThose will spawn a docker-composed environment with two service instances connected to\na postgres container and a load balancer on top. Tests will be executed against the load balancer,\nsimulating a distributed environment, and asserting that the service can scale and remain consistent.\n\nTo run the full suite, run:\n```shell script\n./gradlew check\n```\n\n### Running\n\nThe service can be spawned in a minimal production-like environment using `docker compose`.\nThe environment consists of two service instances packaged in a docker container, connected to a Postgres container, and\nexposed via Envoy Proxy. A minimal monitoring setup is available, as well.\n\nTo start:\n```shell script\n./gradlew composeUp\n```\nThe service will be accessible on localhost:8080 and requests\nwill go via a load balancer to two service instances in a round-robin fashion.\n\nTo stop:\n```shell script\n./gradlew composeDown\n```\n\n### Observability\n\nThe service is configured to run with an opentelemetry-java agent which collects and forwards telemetry (metrics, logs, traces)\nto a configured opentelemetry-collector service (via OTEL_EXPORTER_OTLP_ENDPOINT environment variable).\n\nThe provided docker-compose.yml configures the environment with fully provisioned observability stack that\ncontains the opentelemetry-collector which in turn routes logs to Loki, metrics to Prometheus, and traces to Tempo.\n\nGrafana is available on port 3000. Dashboards are available for basic service metrics, JVM metrics, and Envoy metrics.\nIt is pretty cool to see how light the service is even when stressed:\n![memory footprint](docs/memory_footprint.png)\n\nLogs and traces are available in Grafana via Loki and Tempo data sources - use the Explore view.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frieske%2Fevent-sourced-account","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frieske%2Fevent-sourced-account","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frieske%2Fevent-sourced-account/lists"}