This is a training project to learn how to implement backend by using Kotlin, Spring Boot and Postgresql.
You will build a bank application in the end of this training.
A bank can have customers and accounts. A customer can have multiple accounts.
This project requires basic Kotlin and Postgresql knowledge.
Use Kotlin Koans to learn Kotlin.
Run BankApplication.kt
to start the application.
An alternative way is to run ./gradlew bootRun
in the terminal.
Implement API specs for customer. The endpoints can return dummy data for now.
Use Spring's RestController to implement the API.
A customer has a name, birthdate, address, phone number and email address.
APIs:
- Create a customer
- Get customer by id
- Search customers by name
Implement API specs for account. The endpoints can return dummy data for now.
An account has a name, balance, currency and customer id.
APIs:
- Create an account
- Get accounts by customer id
- Deposit money to an account
- Withdraw money from an account
Implement validations for the API request. Investigate how to use @ExceptionHandler and @ControllerAdvice.
Customer validations
- Name should not be blank
- Birthdate should be before now
- Address should not be blank
- Email should have a valid format
- Phone number should have a valid format
Account validations
- Currency should be a valid currency
- Customer id should exist
- Deposit account should exist
- Deposit amount should be bigger than 0
- Deposit currency should be same as account currency
- Withdraw account should exist
- Withdraw amount should be bigger than 0
- Withdraw amount should be less or equal to the account balance
- Withdraw currency should be same as account currency
Test the validations that you have implemented in Step-3.
Consider using JUnit, WebTestClient
and Kotest assertions.
WebTestClient is already configured in IntegrationBaseTest
.
Example usage:
class HelloControllerTest(
@Autowired private val webTestClient: WebTestClient,
): IntegrationBaseTest() {
@Test
fun `returns hello world`() {
// TODO
}
}
Create customer table with Flyway migration.
Add your SQL script to /src/main/resources/db/migration/
, it will be executed during application startup.
Use JOOQ to implement customer repository with following methods.
- Create customer
- Get customer by id
- Get customers by name
An example repository:
@Repository
class CustomerRepository(private val context: DSLContext) {
}
JOOQ classes are generated into /build/generated-jooq/
.
See JOOQ docs here
Note: Uncomment cleanUpDatabase
in IntegrationBaseTest
after creating your first migration script, so that tables are cleaned up between integration tests.
Create account table with Flyway migration
Use JOOQ to implement account repository with following methods.
- Create an account
- Get accounts by customer id
- Deposit money to an account
- Withdraw money from an account
Relationships
- Customer id should exist in Customer table
Note: Check how foreign keys works in Postgresql here
Write unit tests for your service. With unit tests, you can test your business logic isolated from external dependencies which would run faster than integration tests.
Use mockk to mock the dependencies.
class ExampleServiceTest() {
private val exampleRepository: ExampleRepository = mockk()
val exampleService = ExampleService(exampleRepository)
fun `test example`() {
// Given
every { exampleRepository.get() } returns Example("some example")
...
}
}
Bonus: Consider using value
class for domain model ids, so that we can leverage more from compile time type check.
@JvmInline
value class AccountId(val value: String)
See the value class docs here.
- Use upsert for your insert and update operations in which you can update the record if it already exists.
You can use sql ON CONFLICT for it.
fun upsert(example :Example) = context.insertInto(EXAMPLES) .set(example.toInsertRecord()) .onConflict(EXAMPLES.ID) .doUpdate() .set(example.toUpdateRecord()) .where(EXAMPLES.ID.eq(id)) .returning() .fetchOne()
We got a feedback from users that when they search with customer name that it takes too long. Try to improve search by customer name by using index.
Consider using GIN index explained here
Use explain analyze
to check the execution plan for your query.
See https://www.postgresql.org/docs/current/using-explain.html#USING-EXPLAIN-ANALYZE
To generate data for your Customer
table you can use mockaroo:
- Add your table fields with related types for them;
- Specify how many rows of data you need (max: 1000 row);
- Select format to
SQL
- Add your table name (
CUSTOMER
) - Click on
Generate Data
button.
Last week, two users withdrew money from the same bank account simultaneously and the account balance went to negative. This is a financial risk for our bank, and we want to prevent it happening again.
Consider using optimistic locking described here to handle concurrent operations.
A simple approach to implement optimistic locking is
- Add a
version
column to your table. - Increment
version
with each update. - Get
version
before each update. - Execute the update query with
where
condition version equals gathered version. - When the update query does not return anything, version is changed. Either inform the user or retry.
Last week, a customer was able to withdraw money from an account even though the customer is archived (soft deleted). When we investigated, we found that the customer was archived, but his accounts were not archived. Make sure that when a customer is archived, all of their accounts are also archived.
- Implement archive for customer and account.
- Execute customer and account archiving in a single transaction.
Consider using TransactionTemplate to handle transactions in Spring.
class SomeService(private val transactionTemplate: TransactionTemplate) {
override fun someFunction() {
transactionTemplate.execute {
doDbStuff()
doMoreDbStuff()
}
}
}
- Implement an integration test for the customer and account archiving where customer archiving is successful but account archiving fails. Verify that the customer is not archived in this case. Use @MockkBean or @SpyBean to fail the account archiving in the integration test.
We would like leverage functional programming in our project. Arrow-kt is a library that provides functional programming in Kotlin. Here are the docs for Arrow-kt: https://arrow-kt.io/docs/.
- Replace throwing exceptions on the service layer with Arrow's
Either
type. See the docs here. - On the API layer, you need to throw an exception when the
Either
isLeft
, so that the response can be mapped correctly.