A small financial OTP application for personal studies.
Be reliable, have no bugs, clean code, be scalable, and use the minor third-party libraries as possible. The idea to use fewer third-party libs is not to reinvent the wheel
, but to do most of the code by myself. To achieve these goals I do not doubt that Erlang or Elixir are the right choice, so I'm going with Erlang.
The project is structured to have domain and business logic separated from what it exposes to the world, also has adapters to plug third-party libraries and not have a direct dependency on them. I really enjoy how the Elixir code is structured, so some concepts and directory structures are based on Elixir libraries or applications.
.
|-- src: the source folder, contains the main code.
|-- ebank: holds the business and domain code.
| |-- account: the account entity.
| |-- db: database adapters.
| |-- dsl: query adapters.
| |-- json: JSON parser adapters.
| |-- model: behavior that interacts with the database.
| |-- schema: behavior that holds the database schemas.
| |-- server: server adapters.
|-- ebank_web: expose the ebank directory to the world.
|-- controller: resolves HTTP requests.
This application uses changeset
, an Erlang library created and maintained by me. It provides an interface based on the Elixir/Ecto changeset library.
A changeset is required to manipulate any data in the database, in this way, any data is validated before persists.
The current database used is mnesia. Mnesia it's a built Erlang database that provides:
- A relational/object hybrid data model that is suitable for telecommunications applications.
- A DBMS query language, Query List Comprehension (QLC) as an add-on library.
- Persistence. Tables can be coherently kept on disc and in the main memory.
- Replication. Tables can be replicated at several nodes. Atomic transactions. A series of table manipulation operations can be grouped into a single atomic transaction.
- Location transparency. Programs can be written without knowledge of the actual data location.
- Extremely fast real-time data searches.
- Schema manipulation routines. The DBMS can be reconfigured at runtime without stopping the system.
Mnesia works really well, it's reliable, scalable, and fits great to this simple project.
The query system is provided via DSL
(Domain Specific Language), where Erlang code is compiled into the database syntax. The syntax expects a list of tuples with this format: {{Table, Field}, Operator, Value}
.
- The
Table
and theField
are atoms defined in the schema (see metaprogramming); - The
Operator
is any Erlang operators, like=:=
,=/=
,>
,<
, etc; - The
Value
is any Erlang term or, if it is an atom starting with@
, it will be considered as a variable that can be defined at the runtime, e.g.@foo
.
They can be linked using orelse
and andalso
, and to resolve the variables a map with the index of each field of each table evolved in the query is required, e.g. #{table_a => #{foo => 1}, table_b => #{bar => 1, baz => 2}}
, e.g.:
Table = mytable,
Indexes = #{mytable => #{foo => 1, bar => 2, baz => 3}},
Clauses=[{'andalso', [
{{Table, foo}, '=:=', <<"bar">>}, % <- static
{'orelse', [
{{Table, bar}, '=/=', '@baz'}, % <- variable 'baz'
{{Table, baz}, '>=', 0} % <- static
]}
]}],
Query = ebank_dsl:query(Clauses, Indexes),
ebank_db:read(Query, #{baz => the_baz_value}).
This project uses qlc to resolve mnesia
queries, so the output is a list comprehension:
[ Mytable || Mytable <- mnesia:table(mytable)
, ( element(2, Mytable) =:= <<"bar">>
andalso ( element(3, Mytable) =/= maps:get(baz, Bindings)
orelse element(4, Mytable) >= 0 )
)
]
To illustrate, the result of the query above in PostgreSQL
should be:
WHERE mytable.foo = 'bar'
AND ( mytable.bar != $1 OR mytable.baz >= 0 )
This project use parse_transform
to do metaprogramming.
The reason behind that is to simplify the code and make it more scalable, readable, and easy to maintain.
Currently, the project contains two modules that provide metaprogramming:
ebank_model_transform
: used by the database models. The module that uses it should expose a-model
attribute. Currently, it compiles the queries to improve performance. Example:-model(#{ schema => ebank_account_schema, queries => [ q_fetch_by_id/0, q_exists/0 ] }).
ebank_schema_transform
: used by database schemas. The module that uses it should expose a-schema
attribute. Currently, it compiles the schema and exposes it on aschema/0
function in the module. Out of the box, it provides type definitions and validations via changesets for the fields. Example:-schema(#{ table => account, fields => [ {id, {integer, [readonly]}}, {social_id, {binary, [required, indexed]}}, {name, {binary, [required]}}, {password, {binary, [required, redacted]}}, {created_at, {datetime, [readonly]}} ] }).
NOTE: An Erlang helper library called parserl is used to simplify the metaprogramming. I'm the author and it is maintained by me.
The make prod
command in the root folder starts the application in production mode, make dev
to start in developer mode, or make daemon
to start in developer mode and start a hot code reloading.
The command starts a server at the 8080
port and these routes are exposed:
[POST] /accounts
: create an account if all parameters are valid and only if no one with the desiredsocial_id
exists;[GET] /accounts/:id
: return an account if some exists with the informedid
;[PATCH] /accounts/:id
: update an account if some exists with the informedid
(TODO: allow updates only for authorized/logged user(s));
This is an under-development application. Any change can occur without any notice. There are a lot of things to do and improve.
- Create a table called
transactions
where accounts can share money; - Create a table called
tokens
orsessions
where the user can log in and perform updates and transactions;