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.
For MLH reviewers: Development Notes → 🎖️ documentation.md
Frontend Repo: wallet-frontend
- 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
Wallet.-.A.simple.wallet.app.made.with.Ktor.backend.Framework.and.Kotlin.Frontend.Multiplatform.mp4
Expand to see Setup and Installation
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 |
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
Expand to see Programming Paradigms demonstrated in this Project
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
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