This is a Personal Finance Management System built as a class project for COMP 4411 - Programming Languages at Lakehead University. The project demonstrates various programming paradigms including Object-Oriented, Procedural, and Concurrent programming using Kotlin and the Ktor framework.
Please refer to documentation.md
This project was created using the Ktor Project Generator.
Before running this project, ensure you have the following installed:
-
Java Development Kit (JDK) 21
-
Docker Desktop
- Download and install Docker Desktop
- Ensure Docker is running before proceeding
-
Gradle (Optional - project includes Gradle Wrapper)
- The project uses Gradle Wrapper (
./gradlew), so manual installation is not required
- The project uses Gradle Wrapper (
git clone <repository-url>
cd wallet-backendThis project uses PostgreSQL running in a Docker container for consistent development environments.
Start the PostgreSQL container:
docker run --name my-wallet-db -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 -d postgresVerify the container is running:
docker psYou should see my-wallet-db in the list of running containers.
Important Database Configuration:
- Host:
localhost - Port:
5432 - Database:
postgres - Username:
postgres - Password:
mysecretpassword
Note: These credentials are configured in
src/main/kotlin/Databases.kt. Update them if you use different credentials.
The database connection is configured in Databases.kt:
object Database {
fun connect(): Connection {
val url = "jdbc:postgresql://localhost:5432/postgres"
val user = "postgres"
val password = "mysecretpassword"
return DriverManager.getConnection(url, user, password)
}
}If you need to change credentials:
- Open
src/main/kotlin/Databases.kt - Update the
url,user, andpasswordvalues - Ensure they match your Docker PostgreSQL configuration
./gradlew buildThis will:
- Download all dependencies
- Compile Kotlin code
- Run tests
- Generate build artifacts
./gradlew runThe server will start on http://localhost:8080
Expected console output:
2024-11-XX XX:XX:XX.XXX [main] INFO Application - Application started in 0.303 seconds.
2024-11-XX XX:XX:XX.XXX [main] INFO Application - Responding at http://0.0.0.0:8080
On first startup, the application will:
-
Create custom PostgreSQL types:
transaction_typeENUM ('expense','income')period_typeENUM ('daily','weekly','monthly','yearly')
-
Create tables if they don't exist:
users- User accounts with authenticationtransactions- Financial transaction recordsbudgets- Budget limits and tracking
-
Seed demo data (10 demo users with transactions and budgets)
Verify database tables:
Access PostgreSQL interactive terminal:
docker exec -it my-wallet-db psql -U postgresList all tables:
\dtExit PostgreSQL terminal:
\qHealth check:
curl http://localhost:8080/Expected response: Hello World!
Get transactions for user 1:
curl http://localhost:8080/transactions/1Using Postman or any API client:
- Import the OpenAPI documentation from:
http://localhost:8080/openapi - Test all CRUD endpoints for Users, Transactions, and Budgets
| Command | Description |
|---|---|
docker ps |
List running containers |
docker stop my-wallet-db |
Stop the database container |
docker start my-wallet-db |
Start the database container |
docker rm my-wallet-db |
Remove the container (must be stopped first) |
docker exec -it my-wallet-db psql -U postgres |
Access PostgreSQL CLI |
docker logs my-wallet-db |
View database logs |
- OS: macOS, Linux, or Windows
- RAM: Minimum 4GB (8GB recommended)
- Disk Space: ~500MB for dependencies and Docker images
- Java: JDK 21
- Docker: Latest stable version
If you have PostgreSQL already running locally:
# Stop local PostgreSQL service (macOS)
brew services stop postgresql
# Or use a different port for Docker
docker run --name my-wallet-db -e POSTGRES_PASSWORD=mysecretpassword -p 5433:5432 -d postgresThen update the port in Databases.kt to 5433.
- Verify Docker container is running:
docker ps - Check container logs:
docker logs my-wallet-db - Ensure credentials in
Databases.ktmatch your Docker setup - Try restarting the container:
docker restart my-wallet-db
You may see error messages about types already existing:
Error executing SQL statement: CREATE TYPE transaction_type...
Error executing SQL statement: CREATE TYPE period_type...
This is normal! The application uses CREATE TYPE IF NOT EXISTS logic. On subsequent runs, these types already exist, causing harmless errors that are caught and logged.
# Clean and rebuild
./gradlew clean build
# If Gradle wrapper fails to download
chmod +x gradlew
./gradlew wrapper --gradle-version=8.5- Kotlin - Primary programming language
- Ktor 3.0+ - Asynchronous web framework for building REST APIs
- Gradle - Build automation and dependency management
- PostgreSQL - Relational database management system
- JDBC - Direct database connectivity without ORM
- BCrypt (at.favre.lib:bcrypt:0.10.2) - Password hashing
- kotlinx.serialization - JSON serialization/deserialization
- Logback - Logging framework
- Netty - High-performance asynchronous event-driven network application framework
The project uses OOP principles extensively with data classes, singleton objects, and encapsulation.
Example - Data Models:
// User.kt - Data class with properties
@Serializable
data class User(
val userId: Int? = null,
val firstName: String,
val lastName: String,
val email: String,
val password: String,
val createdAt: String? = null,
val updatedAt: String? = null,
)
// Transaction.kt - Domain model
@Serializable
data class Transaction(
val transactionId: Int? = null,
val userId: Int,
val title: String,
val category: String,
val transactionType: String,
val amount: String,
val date: String
)Example - Repository Pattern:
// UserRepository.kt - Encapsulation of data access logic
class UserRepository {
fun createUser(first: String, last: String, email: String, hashedPassword: String): Int {
return DbHelper.query(
sql = SqlBlueprints.INSERT_USER,
prepare = { ps ->
ps.setString(1, first)
ps.setString(2, last)
ps.setString(3, email)
ps.setString(4, hashedPassword)
},
map = { rs ->
rs.next()
rs.getInt("user_id")
}
)
}
}Example - Singleton Object Pattern:
// Database.kt - Singleton object for database connection
object Database {
fun connect(): Connection {
val url = "jdbc:postgresql://localhost:5432/postgres"
val user = "postgres"
val password = "mysecretpassword"
return DriverManager.getConnection(url, user, password)
}
fun init() {
// Initialize database tables
}
}The project uses procedural approaches for sequential operations and data processing.
Example - Routing Configuration:
// Routing.kt - Procedural route setup
fun Application.configureRouting() {
routing {
userRouting()
transactionRouting()
budgetRouting()
get("/") {
call.respondText("Hello World!")
}
}
}Example - Database Operations:
// TransactionRoutes.kt - Procedural database insert
post {
val transaction = call.receive<Transaction>()
val connection = Database.connect()
val sql = """
INSERT INTO transactions (user_id, title, category, transaction_type, amount, date)
VALUES (?, ?, ?, ?::transaction_type, ?, ?::timestamp with time zone)
""".trimIndent()
connection.use { conn ->
val statement = conn.prepareStatement(sql)
statement.setInt(1, transaction.userId)
statement.setString(2, transaction.title)
statement.setString(3, transaction.category)
statement.setString(5, transaction.transactionType)
statement.setBigDecimal(6, BigDecimal(transaction.amount))
statement.executeUpdate()
}
call.respond(HttpStatusCode.Created, "Transaction stored successfully")
}The project demonstrates thread-based concurrency with proper synchronization mechanisms.
Example - Thread-Safe Demo Data Generation:
// DemoDataSeeder.kt - Concurrent data seeding with thread safety
object DemoDataSeeder {
// Concurrency primitives
private val userInsertLock = ReentrantLock()
private val userIdListRWLock = ReentrantReadWriteLock()
private val sharedUserIdList = mutableListOf<Int>()
// Thread-safe write operation
private fun addUserIdThreadSafely(newId: Int) {
userIdListRWLock.writeLock().withLock {
sharedUserIdList.add(newId)
}
}
// Thread-safe read operation
private fun safelyGetAllUserIdsSnapshot(): List<Int> {
userIdListRWLock.readLock().withLock {
return sharedUserIdList.toList()
}
}
// Creating concurrent worker threads
private fun createConcurrentWorkerThreadsForAllUsers(userIds: List<Int>) {
val allThreads = mutableListOf<Thread>()
for (userId in userIds) {
// One transaction thread per category
for (category in categories) {
val t = Thread({
generateTransactions(userId, category)
}, "Tx-User$userId-$category")
t.start()
allThreads.add(t)
}
// One budget thread per user
val b = Thread({
generateBudgets(userId)
}, "Budget-User$userId")
b.start()
allThreads.add(b)
}
// Wait for all threads to complete
allThreads.forEach { it.join() }
}
}Kotlin's functional programming features are used throughout the project.
Example - Higher-Order Functions:
// DbHelper.kt - Generic query function using lambdas
fun <T> query(
sql: String,
prepare: (PreparedStatement) -> Unit,
map: (ResultSet) -> T
): T {
Database.connect().use { connection ->
connection.prepareStatement(sql).use { ps ->
prepare(ps)
ps.executeQuery().use { rs ->
return map(rs)
}
}
}
}- User Management: Registration, login with BCrypt password hashing, profile updates
- Transaction Tracking: Create, read, update, and delete financial transactions
- Budget Management: Set and monitor spending budgets by category
- RESTful API: Clean REST endpoints for all operations
- Thread-Safe Demo Data: Concurrent data seeding with proper synchronization
- PostgreSQL Integration: Direct JDBC usage with custom SQL queries
- CORS Support: Cross-origin resource sharing for frontend integration
POST /users- Create new userGET /users/login- Authenticate userPUT /users/{id}- Update user profile
POST /transactions- Create transactionGET /transactions/{userId}- Get all transactions for a userPUT /transactions/{id}- Update transactionDELETE /transactions/{id}- Delete transaction
POST /budgets- Create budgetGET /budgets/{userId}- Get all budgets for a userPUT /budgets/{id}- Update budgetDELETE /budgets/{id}- Delete budget
The application uses PostgreSQL with the following custom types and tables:
transaction_type:'expense' | 'income'period_type:'daily' | 'weekly' | 'monthly' | 'yearly'
- users: User accounts with authentication
- transactions: Financial transaction records
- budgets: Budget limits and tracking
- Ktor Documentation
- Ktor GitHub page
- The Ktor Slack chat. You'll need to request an invite to join.
Here's a list of features included in this project:
| Name | Description |
|---|---|
| Routing | Provides a structured routing DSL |
| OpenAPI | Serves OpenAPI documentation |
| CORS | Enables Cross-Origin Resource Sharing (CORS) |
| kotlinx.serialization | Handles JSON serialization using kotlinx.serialization library |
| Content Negotiation | Provides automatic content conversion according to Content-Type and Accept headers |
| Postgres | Adds Postgres database to your application |
To build or run the project, use one of the following tasks:
| Task | Description |
|---|---|
./gradlew test |
Run the tests |
./gradlew build |
Build everything |
./gradlew run |
Run the server |
If the server starts successfully, you'll see the following output:
2024-12-04 14:32:45.584 [main] INFO Application - Application started in 0.303 seconds.
2024-12-04 14:32:45.682 [main] INFO Application - Responding at http://0.0.0.0:8080