A microservice-ready modular monolith backend for a food delivery app, built with Kotlin and Ktor. It is being developed as a reference architecture that can scale from a single deploy to independent microservices without rewriting business logic.
Most food delivery backend tutorials are either too simple (single-file CRUD) or jump straight to microservices (overkill for 99% of projects). This project sits in the sweet spot: a production-grade modular monolith where every module is shaped like a future microservice, with explicit extraction seams documented and enforced by Gradle.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Single JVM Process β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββ β
β β shared/ β β
β β common Β· events Β· auth Β· persistence β β
β βββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β ββββββββββββ ββββββββββββ ββββββββββββ β
β β user β βrestaurantβ β order β β
β β service β β service β β service β β
β ββββββββββββ ββββββββββββ ββββββββββββ β
β ββββββββββββ ββββββββββββ ββββββββββββ β
β β payment β β notific. β β tracking β β
β β service β β service β β service β β
β ββββββββββββ ββββββββββββ ββββββββββββ β
β β β β β
β βββββββββββββββββββββββββββββββββββββββββββββββ β
β β EventBus (SharedFlow) β β
β βββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The target architecture is for each service module to contain its own domain/, infrastructure/, api/, di/, and migrations/. The current repository has a lighter scaffold in place and is moving toward that structure incrementally.
Implemented today:
- Multi-module Gradle build
- Shared
common,events,auth, andpersistencemodules - Service module boundaries for user, restaurant, order, payment, notification, and tracking
- Ktor application shell in
app/ - Placeholder routes and minimal domain models
- Basic integration test for app startup
Planned but not implemented yet:
- PostgreSQL/PostGIS setup
- Redis integration
- Flyway migrations
- Razorpay, Firebase, and S3 integrations
- Swagger/OpenAPI docs
- Micrometer/Prometheus metrics
- Full domain services, repositories, DTOs, and production flows
- Kotest, MockK, and Testcontainers-based test suite
| Layer | Tool |
|---|---|
| Language | Kotlin 2.x |
| Framework | Ktor 3.x (Netty) |
| Serialization | kotlinx.serialization |
| Database | H2 today, PostgreSQL/PostGIS planned |
| ORM | Exposed DSL |
| Migrations | Planned |
| Cache | Planned |
| DI | Koin 4 |
| Event Bus | In-process SharedFlow (swappable to RabbitMQ) |
| Auth | JWT (java-jwt) |
| Payments | Planned |
| Push Notifications | Planned |
| Storage | Planned |
| Observability | Logback today, broader observability planned |
| Testing | Ktor testApplication today, broader test stack planned |
- JDK 17+
- No external infrastructure is required for the current scaffold
# Clone the repo
git clone https://github.com/gautam84/foodike-backend.git
cd foodike-backend
# Run the application
./gradlew :app:runThe server starts at http://localhost:8080.
The current scaffold only uses application config from app/src/main/resources/application.yaml.
Planned environment variables for future infrastructure:
| Variable | Description | Required |
|---|---|---|
DB_URL |
Postgres JDBC URL | Planned |
DB_USER |
Postgres username | Planned |
DB_PASSWORD |
Postgres password | Planned |
REDIS_URL |
Redis connection URL | Planned |
JWT_SECRET |
Secret for signing JWTs | Planned |
JWT_ISSUER |
JWT issuer claim | Planned |
RAZORPAY_KEY |
Razorpay API key | For payments |
RAZORPAY_SECRET |
Razorpay API secret | For payments |
FIREBASE_CREDENTIALS |
Path to Firebase service account JSON | For notifications |
S3_BUCKET |
S3 bucket name for image uploads | For storage |
S3_REGION |
AWS region | For storage |
# Unit tests
./gradlew :tests:test --tests "com.example.foodike.unit.*"
# All tests
./gradlew :tests:testfoodike-backend/
βββ shared/
β βββ common/ # Value objects, exceptions, utilities
β βββ events/ # Event contracts + EventBus interface
β βββ auth/ # JWT validation, shared across all modules
β βββ persistence/ # Database factory, query helpers
β
βββ services/
β βββ user-service/ # Auth (OTP), profiles, addresses
β βββ restaurant-service/ # Restaurants, menus, search
β βββ order-service/ # Cart, orders, coupons
β βββ payment-service/ # Razorpay integration
β βββ notification-service/ # FCM, SMS, email
β βββ tracking-service/ # WebSocket order tracking
β
βββ app/ # Ktor entry point, plugin config, Koin wiring
βββ tests/ # Unit + integration tests
Target per-service structure:
services/xyz-service/
βββ domain/
β βββ model/ # Entities and value objects
β βββ repository/ # Interfaces (ports)
β βββ service/ # Business logic
β βββ port/ # Cross-service query interfaces (if needed)
βββ infrastructure/
β βββ persistence/
β β βββ tables/ # Exposed table definitions
β β βββ *Impl.kt # Repository implementations
β βββ adapter/ # In-process adapters for query ports
β βββ events/ # Event publishers and consumers
βββ api/
β βββ routes/ # Ktor route definitions
β βββ dto/ # Request/response objects
β βββ mapper/ # Domain β DTO mappers
βββ di/
β βββ XyzModule.kt # Koin module
βββ migrations/
βββ db/migration/ # Flyway SQL files
shared/common β no dependencies
shared/events β common
shared/auth β common
shared/persistence β common
services/* β shared modules only (NEVER other services)
app β all services (composition root)
The critical rule: service modules should never depend on each other. The project is structured around that rule, and module dependencies should continue to preserve it. If order-service needs data from restaurant-service, it should go through a query port interface rather than a direct import.
This architecture is intended so that extracting any module into an independent microservice can happen with minimal business-logic changes. These seams are part of the target design and are only partially implemented today:
Services communicate asynchronously through EventBus. Today the repo includes an in-process event bus. A broker-backed event bus is planned for extraction scenarios.
// Today (monolith)
single<EventBus> { InProcessEventBus() }
// Planned after extraction
single<EventBus> { RabbitMqEventBus(connectionFactory) }When a service needs synchronous data from another service, it should call a port interface. The concrete in-process and HTTP adapters shown below are target examples and are not fully implemented yet.
// Target monolith form
single<MenuQueryPort> { InProcessMenuAdapter(get()) }
// Planned extraction form
single<MenuQueryPort> { HttpMenuClient(httpClient, env("RESTAURANT_SERVICE_URL")) }Each service is intended to own its own migrations. Flyway integration is planned and not wired into the current scaffold yet.
To extract any service into an independent microservice:
- Copy
services/xyz-service/into its own Gradle project - Add its own
Application.ktandDockerfile - Swap
InProcessEventBusβRabbitMqEventBus - Swap
InProcess*AdapterβHttp*Clientfor query ports - Point Flyway at a dedicated database
- Add to API gateway route config
- Remove from monolith's
settings.gradle.ktsandAppModule.kt
POST /auth/send-otpβ send OTP to phonePOST /auth/verify-otpβ verify and get JWT pairPOST /auth/refreshβ refresh access token
GET /users/meβ current profilePUT /users/meβ update profileGET /users/me/addressesβ list addressesPOST /users/me/addressesβ add addressDELETE /users/me/addresses/{id}β remove address
GET /restaurantsβ list (paginated, filterable)GET /restaurants/nearby?lat=&lng=&radius=β geo queryGET /restaurants/{id}β detailGET /restaurants/{id}/menuβ grouped menu
GET /cartβ current cartPOST /cart/itemsβ add itemPUT /cart/items/{id}β update quantityDELETE /cart/items/{id}β remove itemDELETE /cartβ clearPOST /cart/couponβ apply coupon
POST /ordersβ place orderGET /ordersβ list (paginated)GET /orders/{id}β detailPATCH /orders/{id}/statusβ update statusWS /track/{orderId}β live tracking
POST /payments/initiateβ create Razorpay orderPOST /payments/verifyβ verify payment signature
GET /search?q=&type=β full-text search
The endpoint list above reflects the target API surface. The current implementation only exposes a small subset of placeholder routes while the modules are being built out.
This project is licensed under the MIT License. See LICENSE.md for details.
Contributions are welcome! Please read CONTRIBUTING.md before submitting a PR.