Online Banking app with different account types
- Banking App
- Backend
- Java 21
- Maven
- Frontend
- Node.js
- User Authentication/Authorization
- Email registration
- JWT tokens for authentication
- Different user groups with different privileges
- Account
- CRUD operations
- Checking/Savings Accounts
- CRUD operations
- Transfers
- Annual interest calculation
- Interest rate management
- List and filter account transactions
This repository is a monorepository containing frontend and backend of the banking app. The backend is implemented with Spring Boot 3 and the frontend with Angular 17.
In this project, I used various Git features. I choose to use a monorepository for both applications. The project has a production branch (main) and feature branches (example), which can only be merged into the main branch through a pull request with atleast one review. Therefore, it is not possible to push directly into the main branch (branch protection).
I utilized different (uncommon) git features such as squashing commits, particularly when testing pipeline features with multiple small test commits. Additionally, I rebased most branches after changes on the main branch instead of merging to get a cleaner commit history.
I used StarUML to make UML diagrams and experimented with PlantUML. However, arranging graphical components in my desired order was a bit tricky with PlantUML, which is why I stuck to StarUML. Overall, I created diagrams for deployment, components, use cases, and activities.
First, I made a use case diagram showing what functions the system should have and which actors can use them.
The deployment diagram shows different hardware components, like clients connecting to the banking servers. It also has distributed database parts to prevent losing data. Besides the main server, there are components in each bank branch, like ATMs communicating to the main systems.
The component diagram illustrates the main components needed for the system and their relationships.
The activity diagram explains how a customer registers with the banking app, including extra steps for user identification like POSTIDENT.
I used Miro for event storming. At first, I brainstormed domain events and potential conflicts that could arise in the app.
Then, I categorized these terms into different processes and further grouped them within the processes into smaller subsets.
I assigned individual names to these groups and organized them in a Core Domain Chart. As shown in the image, my core domains primarily revolve around financial aspects such as trading and transferring money. These are intended to be supported by analysis sub-domains, which enhance the customer experience but are not essential. Tasks like withdrawing money from ATMs can be outsourced to third parties (generic domain).
In the final step, I added relationships. The analysis domains are downstream of the core domains, which provides information for them, for example in trading. The money transfer and financial services have a shared core, they both require access to money transfer data and analytical data for proper functionality. This shared core enables both teams to easily communicate and access shared data. Example use case: Customers might receive recommendations for optimizing their spending behavior.
Core Domain Chart with relations
The images shown here are from an early development stage of the backend. For the metrics, I used Sonarqube, which I set up using Docker. However, I chose not to put it into my pipelines because running an additional Sonarqube server for each pull request seemed impractical for this simple project.
To gather metrics, I used Sonarqube, which I set up using Docker. Nevertheless, I decided against integrating it into my pipelines because running an extra Sonarqube server for each pull request appeared impractical for this project.
Simple Docker Sonarqube setup, which i used:
docker run -d --name sonarqube -e SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true -p 9000:9000 sonarqube:latest
Login: admin
Password: admin
I used JaCoCo to generate Java test coverage and integrated it into my CI/CD pipeline.
For frontend test coverage, I used Karma with Jasmine.
Karma & Jasmine Coverage report
In my Frontend pipeline I also run a linter over my code to ensure stylistic correctness.
Furthermore, I integrated Grype security checks into both pipelines. Grype can scan dependency files such as pom.xml and packages from various tools and programming languages for vulnerabilities.
MY PERSONAL CLEAN CODE CHEAT SHEET
- Example: Commit 1, Commit 2
- Explanation:
- In both examples, we can see how I extracted common code into a function. This avoids redundancy and makes the maintenance of the code simpler. In the first commit, I moved the building of the verification redirect link into a separate function and in the second, I moved the process of getting the user from a repository into a function, which also handles common edge cases.
- Example: Commit
- Explanation:
- Descriptive and precise variable names contribute significantly to code readability. In this commit, the variable name ACCOUNT was replaced with a more precise name, USER_ACCOUNT. I made this change because there are both banking accounts and user accounts, and using only ACCOUNT as the table name would be too unprecise.
- Example: Commit 1, Commit 2
- Explanation:
- Eliminating dead code, which is unreachable or no longer serves a purpose, is good practice for maintaining a clean codebase. In these commits, I removed unused imports, which can also eliminate potential bugs in other dependencies that are not used in this class now.
- Example: Commit
- Explanation:
- A clear separation of concerns is important for maintaining modular and maintainable code. In this commit, I moved a function to the Currency enum that can read an enum from a String. This functionality was previously within a service function. This separation improves code organization and allows for easier testing.
- Example: Function 1, Function 2
- Explanation:
- Lengthy and convoluted conditions can worsen code readability. In these functions, conditions and calculations were moved into separate functions, making the overall conditions and calculations easier to understand.
- Example: Commit 1, Commit 2
- Explanation:
- Robust code includes handling of edge cases and exceptions. In these examples, proper handling of authorization edge cases in UserDetailServiceImpl.java and a centralized exception handling approach in GeneralExceptionHandler.java were implemented.
- Example: Commit
- Explanation:
- The use of magic numbers can make code less maintainable and prone to errors. In this example, I replaced the magic numbers with configuration properties, making it possible to define these values in the configuration file. This enhances code readability and allows for future modifications without directly altering the code.
- Example: Example Service
- Explanation:
- Ensuring that only necessary functions are marked as public and are accessible by other classes is essential for encapsulated code. By restricting public access to only the required functions, we enforce the principle of least privilege, minimizing the potential for unintended usage of internal functions. I tried to implement this practice in all of my classes, especially in the service classes.
For backend build management, I used Maven. In my pom.xml file, I defined a "dev" profile that starts a local H2 (in-memory) database. Additionally, the pom defines the project dependencies. These can be installed using mvn install
. And listed with mvn dependency:tree
.
[INFO] de.eric:bankingapp:jar:0.0.1-SNAPSHOT
[INFO] +- org.springframework.boot:spring-boot-starter:jar:3.2.0:compile
[INFO] | +- org.springframework.boot:spring-boot:jar:3.2.0:compile
[INFO] | | \- org.springframework:spring-context:jar:6.1.1:compile
[INFO] | +- org.springframework.boot:spring-boot-autoconfigure:jar:3.2.0:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-logging:jar:3.2.0:compile
[INFO] | | +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.21.1:compile
[INFO] | | | \- org.apache.logging.log4j:log4j-api:jar:2.21.1:compile
[INFO] | | \- org.slf4j:jul-to-slf4j:jar:2.0.9:compile
[INFO] | +- jakarta.annotation:jakarta.annotation-api:jar:2.1.1:compile
[INFO] | +- org.springframework:spring-core:jar:6.1.1:compile
[INFO] | | \- org.springframework:spring-jcl:jar:6.1.1:compile
[INFO] | \- org.yaml:snakeyaml:jar:2.2:compile
[INFO] +- org.springframework.boot:spring-boot-starter-data-jpa:jar:3.2.0:compile
...
Dependency Tree
The mvn package
command is used to compile the project and package it into a specified file format (in my case .jar). To generate documentation, I specified a dependency that can create Swagger docs from my endpoints. To determine the test coverage, I used a plugin called JaCoCo.
Swagger Docs (http://localhost:8080/swagger-ui/index.html)
For frontend development, I used npm, a package manager and the Angular CLI tool.
All of these tools are utilized in my CI/CD pipelines, which are explained in the following chapter.
In this step, it became noticeable that I am using a monorepo instead of individual repositories. This has made it more complicated to execute different (Github)actions for the frontend and the backend because many actions are performed on the root directory. This results in problems such as a missing pom.xml in the action. However, I have found some workarounds for this (pom.xml example: directory parameter). For future projects, I would definitely use separate repositories, as most (Github)actions are not designed for monorepos. Both pipelines can be triggered either manually in GitHub (using workflow_dispatch) or automatically when there are changes in the respective directory.
Backend pipeline actions:
- Set up Java 21
- Build JAR package and run tests (maven package)
- Update dependency graph in Github (displaying all project dependencies)
- Create a test coverage report using JaCoCo and write the test coverage report in the job summary
- Perform a Grype security check on dependencies, examining all used dependencies for vulnerabilities
Frontend pipeline actions:
- Download Chrome driver to execute Angular Jasmine/Karma tests
- Set up Node.js environment
- Install dependencies
- Run Karma/Jasmine tests with a code coverage report
- Perform code linting and write code coverage from Karma/Jasmine and linting report in the job summary
- Run Cypress e2e tests
- Perform a Grype security check on dependencies, although I have not set it to fail on vulnerabilities. This is because in JavaScript/TypeScript, seemingly everything has a vulnerability.
I have integrated unit tests in both the frontend and backend. In both cases, all unit tests are executed in my pipelines. Additionally, two different types of unit tests have been used for the frontend: component tests with Karma/Jasmine, which are defined in the components (see example) and end-to-end tests with Cypress.
I used IntelliJ and Vscode. I used IntelliJ for the Java application because IntelliJ has a very nice UI to execute various Maven and Java commands and automatically handles some tasks. I also used Vscode for the Angular frontend and to test the HTTP requests because there is a Vscode extension for it and using such an extension in IntelliJ requires premium :( (without this extension I would need Postman).
Favorite Shortcuts Overall:
- Ctrl + Shift + F, to search for something in all project files
Vscode:
- Ctrl + Shift + P, to execute Vscode actions (for example to reload Vscode)
- Ctrl + ., for autocomplete and code action
- Ctrl + Shift + Ö, to open the terminal
- Shift + Alt + F, autoformat
IntelliJ:
- Ctrl + Alt + L, autoformat
- Ctrl + Shift + A, to open IntelliJ Actions
- Ctrl + Alt + O, to optimize imports / remove unused ones
- Alt + Enter, to perform code actions
I use these shortcuts the most while working. However, there are also other useful shortcuts that I sometimes use, but not nearly as often as these.
I implemented the DSLs example for transactions in the banking app, where I used a builder pattern and a fluent interface, which allows method chaining.
I wrote this boilerplate code specifically for the example. Typically, for such scenarios, one would use libraries like Lombok, which provide a @Builder annotation to automatically generate a builder pattern (Lombok example). However, if you need special chaining logic or validations, it can still be useful to create your own pattern instead of using Lombok.
For this task, I programmed a TicTacToe console game in Clojure. Aspects of functional programming:
- only final data structures
- In clojure all data structures are immutable
- (mostly) side-effect-free functions
- Does not have side effects except print statements (if they count)
- An example for a side-effect-free function is the 'execute-move' function, it takes the board player and field and returns a new board with the new executed move
(defn execute-move [player curr-player board field] (let [row (quot field (count board)) col (mod field (count board))] (println (format "%s placing %s in col %d and row %d" (if (= player curr-player) "You are""Enemy is") curr-player col row)) (update-in board [row col] (constantly curr-player)) ) )
- the use of higher-order functions
- I used higher order functions like map and every?, these functions take other functions as parameters
- My ask-for-field also takes an function as parameters for the player input
- functions as parameters and return values
- The create-tic-tac-toe returns a function with a user/programmer defined TicTacToe board size, which can be used to create multiple games of TicTacToe
- use closures / anonymous functions
- The create-tic-tac-toe is also an example for a closure, the return function remembers the create-tic-tac-toe input parameter
- I use anonymous functions in many higher-order functions, like every?. For example, in the game over function to check if every field has been used
(defn game-over? [board] (every? (fn [row] (every? #{"X" "O"} row)) board) )
- DSL Create a small DSL Demo example snippet in your code even if it does not contribute to your project (hence it can also be in another language).
- Integrate some nice unit tests in your Code to be integrated into the Build
- Clean Code Development: A) At least 5 points you can show me with an explanation of why this is clean code in your code and/or what has improved & B) >>10 points on your personal CCD cheat sheet. E.g. a PDF.
- Functional Programming: prove that you have covered all functional aspects in your code as:
- only final data structures
- (mostly) side-effect-free functions
- the use of higher-order functions
- functions as parameters and return values
- use closures / anonymous functions
- You can also do it outside of your project. Even in other languages such as F#, Clojure, Julia, etc.
- Use and understand Git!
- UML at least 3 good different diagrams. "good" means you can pump it up artificially as written in DDD. You have 10 million $ from me! Please export the pics. I can not install all the tools to view them!
- DDD If your domain is too small, invent other domains around and document these domains (as if you have 100 Mio € from Edlich-Investment!) Develop a clear strategic design with mappings/relationships with >4 Domains coming from an Event Storming. Drop your Domains into a Core Domain Chart and indicate the Relations between the Domains!
- Metrics at least two. Sonarcube would be great. Other non-trivial metrics are also fine.
- Build Management with any Build System as Ant, Maven, Gradle, etc. (only Travis is perhaps not enough) Do e.g. generate Docs, call tests, etc. (it could be also disconnected from the project just to learn a build tool!)
- Continuous Delivery: show me your pipeline using e.g. Jenkins, Travis-CI, Circle-CI, GitHub Action, GitLab CI, etc. E.g. you can also use Jenkins Pipelining or BlueOcean, etc. But at least insert more than 2 script calls as done in the lecture! (e.g. also call Ant or Gradle or something else).
- Use a good IDE and get fluent with it: e.g. IntelliJ. What are your favourite key shortcuts?!