In this final part, we'll explore how to set up, run, and test the complete application across different platforms.
Prerequisites
Before running the application, ensure you have:
-
RabbitMQ Server:
- Running locally or accessible via network
- Default connection:
localhost:5672withguest:guest - You can run RabbitMQ using Docker:
docker run -d --name rabbitmq \ -p 5672:5672 \ -p 15672:15672 \ rabbitmq:3-management -
Firebase Service Account:
- Download from Firebase Console → Project Settings → Service Accounts
- Save as
firebase-admin-sdk.jsonin the project root - Or specify custom path via
SERVICE_ACCOUNT_PATHenvironment variable
-
FCM Device Token:
- Obtain from a mobile app integrated with Firebase Cloud Messaging
- Needed to test actual notification sending
Running the Application
Running on JVM (Development)
The JVM target is ideal for development:
./gradlew jvmRun
This command:
- Compiles the Kotlin code for JVM
- Starts the Ktor server on port 8080
- Enables hot reload (in development mode)
- Provides better logging output via Logback
Running on Native Platforms
For production deployment, use native executables:
macOS (ARM64):
./gradlew runDebugExecutableMacosArm64
Linux (x64):
./gradlew runDebugExecutableLinuxX64
Windows (x64):
./gradlew runDebugExecutableMingwX64
The application will:
- Connect to RabbitMQ
- Declare exchange and queue
- Start consuming messages from the queue
- Start the HTTP server on port 8080
You should see output similar to:
[main] INFO Application - Application started
[main] INFO Application - Responding at http://0.0.0.0:8080
Building Production Executables
To build optimized native executables:
./gradlew build
Executables will be located at:
- macOS:
build/bin/macosArm64/releaseExecutable/ktor-native-worker-tutorial.kexe - Linux:
build/bin/linuxX64/releaseExecutable/ktor-native-worker-tutorial.kexe - Windows:
build/bin/mingwX64/releaseExecutable/ktor-native-worker-tutorial.exe
These are standalone executables that can be deployed without a JVM.
Environment Configuration
Configure the application using environment variables:
export RABBITMQ_HOST=localhost
export RABBITMQ_PORT=5672
export RABBITMQ_USER=guest
export RABBITMQ_PASSWORD=guest
export SERVICE_ACCOUNT_PATH=/path/to/firebase-admin-sdk.json
Then run:
./gradlew jvmRun
Or for native executables:
export SERVICE_ACCOUNT_PATH=/path/to/firebase-admin-sdk.json
./build/bin/macosArm64/releaseExecutable/ktor-native-worker-tutorial.kexe
Testing the API
Using curl
Send a test notification request:
curl -X POST http://localhost:8080/api/notifications \
-H "Content-Type: application/json" \
-d '{
"token": "your-fcm-device-token-here",
"title": "Hello World",
"body": "This is a test notification from Ktor Native!"
}'
Expected response:
HTTP/1.1 200 OK
Using HTTPie
If you prefer HTTPie:
http POST localhost:8080/api/notifications \
token=your-fcm-device-token-here \
title="Hello World" \
body="This is a test notification from Ktor Native!"
Using Postman
- Create a new POST request
- URL:
http://localhost:8080/api/notifications - Headers:
Content-Type: application/json - Body (raw JSON):
{
"token": "your-fcm-device-token-here",
"title": "Hello World",
"body": "This is a test notification from Ktor Native!"
}
- Send the request
Understanding the Flow
When you send a request, here's what happens:
-
HTTP Request Received:
- Ktor receives POST to
/api/notifications - Content negotiation deserializes JSON to
NotificationPayload
- Ktor receives POST to
-
Message Published:
- Route handler creates
SendNotificationEvent - Event serialized to JSON
- Published to RabbitMQ exchange with routing key
- HTTP 200 OK returned immediately
- Route handler creates
-
Message Queued:
- RabbitMQ routes message to the queue
- Message persisted to disk (durable queue)
-
Message Consumed:
- Application's consumer receives the message
-
SendNotificationHandlerinvoked
-
Notification Sent:
- Handler deserializes
SendNotificationEvent - Calls
NotificationService.sendNotification() - FCM API sends push notification to device
- Handler deserializes
Monitoring RabbitMQ
If you're running RabbitMQ with the management plugin:
- Open
http://localhost:15672in your browser - Login with
guest:guest - Navigate to "Queues" tab
- You should see
notifications_queue - View message rates, consumers, and queue depth
Verifying the Setup
Check RabbitMQ Connection
After starting the application, verify RabbitMQ connection:
- In RabbitMQ Management UI, go to "Connections"
- You should see an active connection from your application
Check Exchange and Queue
- Go to "Exchanges" tab
- Verify
notifications_exchangeexists (type: topic) - Go to "Queues" tab
- Verify
notifications_queueexists (durable: true) - Check that it's bound to the exchange with routing key
notifications.send
Check Consumer
- In the queue details for
notifications_queue - Scroll to "Consumers" section
- You should see 1 active consumer
Testing Error Scenarios
Invalid JSON
curl -X POST http://localhost:8080/api/notifications \
-H "Content-Type: application/json" \
-d '{"invalid": "data"}'
Expected: 400 Bad Request (missing required fields)
Missing Content-Type
curl -X POST http://localhost:8080/api/notifications \
-d '{"token": "xxx", "title": "Hi", "body": "Test"}'
Expected: 415 Unsupported Media Type
Invalid FCM Token
curl -X POST http://localhost:8080/api/notifications \
-H "Content-Type: application/json" \
-d '{
"token": "invalid-token",
"title": "Test",
"body": "This will fail"
}'
Expected: 200 OK (request accepted), but notification sending will fail in the worker. Check application logs for FCM
error.
Performance Testing
Simple Load Test with curl
for i in {1..100}; do
curl -X POST http://localhost:8080/api/notifications \
-H "Content-Type: application/json" \
-d "{\"token\": \"test-$i\", \"title\": \"Test $i\", \"body\": \"Message $i\"}" &
done
wait
This sends 100 concurrent requests. Monitor:
- RabbitMQ queue depth
- Application memory usage
- Response times
Using Apache Bench
ab -n 1000 -c 10 -p payload.json -T application/json \
http://localhost:8080/api/notifications
Create payload.json:
{
"token": "test-token",
"title": "Load Test",
"body": "Testing"
}
Comparing JVM vs Native Performance
Startup Time
JVM:
time ./gradlew jvmRun
Native:
time ./build/bin/macosArm64/releaseExecutable/ktor-native-worker-tutorial.kexe
Native executables typically start much faster than JVM.
Memory Usage
Monitor memory usage while running:
On macOS/Linux:
# JVM
ps aux | grep java
# Native
ps aux | grep ktor-native-worker-tutorial
Native executables typically use less memory than JVM.
Request Throughput
Use Apache Bench or a similar tool to compare request handling performance between JVM and native builds.
Deployment Considerations
Docker Deployment
Create a Dockerfile for native builds:
FROM alpine:latest
# Install dependencies (if any needed by your native binary)
RUN apk add --no-cache libstdc++
# Copy the native executable
COPY build/bin/linuxX64/releaseExecutable/ktor-native-worker-tutorial.kexe /app/server
# Copy Firebase service account
COPY firebase-admin-sdk.json /app/
WORKDIR /app
# Set environment variables
ENV RABBITMQ_HOST=rabbitmq
ENV SERVICE_ACCOUNT_PATH=/app/firebase-admin-sdk.json
# Run the executable
CMD ["/app/server"]
Build and run:
docker build -t ktor-worker .
docker run -p 8080:8080 --link rabbitmq ktor-worker
Kubernetes Deployment
The native executable is perfect for Kubernetes:
- Fast startup time
- Low memory footprint
- No JVM overhead
- Small container image size
Scaling Workers
You can run multiple instances of the application:
- They'll all consume from the same RabbitMQ queue
- Messages are distributed among consumers
- Enables horizontal scaling
- Provides high availability
Troubleshooting
Application Won't Start
Check RabbitMQ Connection:
# Verify RabbitMQ is running
docker ps | grep rabbitmq
# Check connectivity
telnet localhost 5672
Check Firebase Service Account:
# Verify file exists
ls -la firebase-admin-sdk.json
# Verify it's valid JSON
cat firebase-admin-sdk.json | python -m json.tool
Messages Not Being Processed
Check Consumer Status:
- Open RabbitMQ Management UI
- Verify consumer is connected to the queue
- Check application logs for errors
Check Queue Bindings:
- Verify routing key matches:
notifications.send - Ensure exchange type is
topic
Notifications Not Received
Verify FCM Token:
- Ensure the token is valid and current
- FCM tokens can expire or change
Check Application Logs:
- Look for FCM API errors
- Verify service account has correct permissions
Summary
This tutorial covered:
- Running the application on JVM and Native platforms
- Building production-ready native executables
- Configuring via environment variables
- Testing the API with various tools
- Monitoring RabbitMQ
- Performance testing and comparison
- Deployment strategies
- Troubleshooting common issues
Complete Architecture Overview
Putting it all together:
- Gradle Setup: Multiplatform build with JVM and Native targets
- Notification Service: FCM integration using Flareon library
- AMQP Setup: RabbitMQ integration with Kourier client
- Routes: Type-safe HTTP endpoints with Ktor
- Koin DI: Wiring all components together
- Testing: Running and verifying the complete system
The result is a production-ready, native-compiled backend that:
- Handles HTTP requests asynchronously
- Processes notifications via message queue
- Scales horizontally
- Deploys with minimal resources
- Follows clean code principles
Congratulations on completing the tutorial!
Top comments (0)