Application structure
Ktor applications can be organized in several ways depending on project size, domain complexity, and deployment environment. While Ktor is intentionally unopinionated, there are common patterns and best practices that help keep your application modular, testable, and easy to extend.
This topic describes common structures used in Ktor projects and provides practical recommendations for choosing and applying one.
Default project structure
When you generate a Ktor project using the Ktor project generator, the resulting project uses a single-module structure. This layout is minimal and intended to get you up and running quickly with a working Ktor application.
Although suitable for small applications, this structure does not scale well as the project grows. For larger projects, it is recommended to organize functionality into logical packages and modules, as described in the following sections.
Choosing an application structure
Selecting the right structure depends on the characteristics of your service:
Small services often work well with only a few modules and simple dependency injection.
Medium-sized applications typically benefit from a consistent feature-based structure that groups related routes, services, and data models together.
Large or domain-heavy systems can adopt a domain-driven approach, which provides clearer boundaries and organizes business logic around domain concepts.
Microservice architectures normally use a hybrid style, where each service represents a domain slice and is internally modular.
It’s worth noting that these structures are not mutually exclusive. You can combine multiple approaches — for example, using a feature-based organization within a domain-driven architecture, or applying modularity in a microservice-oriented system.
Layered structure
A layered architecture separates your application into distinct responsibilities: configuration, plugins, routes, business logic, persistence, domain models, and data transfer objects (DTOs). This approach is common in enterprise applications and provides a clear starting point for maintainable code.
Modular architecture
Ktor encourages modular design by allowing you to define multiple application modules. A module is a function extending Application that configures part of the application:
Each module can install plugins, configure routes, register services, or integrate infrastructure components. Modules can depend on each other or remain fully independent, which makes this structure flexible for both monoliths and microservices.
Dependencies are typically injected at module boundaries:
A modular structure helps you:
Separate concerns and isolate feature logic
Enable configuration or plugin installation only where needed
Improve testability by instantiating modules in isolation
Support microservice-friendly or plugin-friendly code organization
Introduce dependency injection at module boundaries
A typical multi-module structure might look like this:
Below is an example build.gradle.kts file for the server/banking module:
In this structure, the banking module does not compile against any database implementation. It only depends on db/core, keeping the domain separate from infrastructure details.
Feature-based modules
Feature-based organization groups code by feature or vertical slice. Each feature becomes a self-contained module, containing its routes, services, data transfer objects (DTOs) and domain logic.
This structure scales well in medium-to-large monoliths or when splitting individual features into microservices later. Each feature can be migrated or versioned independently. A typical feature module may look like this:
In the above example, the module has no knowledge of how CustomerService is created — it only receives it, which keeps dependencies explicit.
Domain-driven design (DDD) approach
A domain-driven structure organizes your application around the core business capabilities it represents. For large projects with complex business rules, it is helpful to separate domain logic from transport, persistence, and infrastructure concerns:
Domain layer
The domain layer remains independent of Ktor. It defines the business rules through the following elements:
Entities represent identifiable domain objects:
Value objects express immutable concepts such as identifiers or validated fields:
Aggregates group related entities under a single consistency boundary:
Repositories abstract persistence and expose operations for retrieving or saving aggregates. Their implementations live in the infrastructure layer, but the interfaces belong to the domain.
Domain services coordinate business logic that spans multiple aggregates or does not naturally belong to a single entity.
Domain events represent meaningful business changes. They allow other parts of the system to react to these events without directly coupling to the service that produced them.
These elements together support a rich domain model while keeping infrastructure details separate.
Application and routing layer
You expose each domain through its own route file or module function, injecting services that manage both logic and state:
Microservice-oriented structure
Ktor applications can be organized as microservices, where each service is a self-contained module that can be deployed independently.
Microservice repositories often use a hybrid of modular architecture, DDD for domain isolation and Gradle multi-module builds for infrastructure isolation.
In this structure, each service owns an isolated domain slice and remains modular internally, integrating with service discovery, metrics, and external configuration.
Entry points
Ktor provides ready-made engine entry points, such as:
When using a canned engine main function, you do not need to define a custom main() method or a dedicated Application.kt entry-point file.
Application modules can be defined in any source file and are loaded by the engine based on configuration.
Modulith deployment
Instead of fully independent microservices, multiple Gradle modules representing services can be packaged independently but deployed together in a single Ktor application. This approach is commonly referred to as a modulith.
Each Gradle module remains internally isolated and exposes an application module that can be loaded through configuration: