Dependency Injection with Kora¶
This guide introduces dependency injection and inversion of control through Kora's compile-time container. It covers how application objects declare dependencies through constructors, how @Component
and @Module make those objects available to the graph, and how Kora validates wiring during compilation instead of discovering missing dependencies at runtime. You will also see why compile-time DI
changes startup behavior, type safety, and testability.
If you want to check your progress along the way, use the finished working example: Kora Java Dependency Injection Introduction App.
If you want to check your progress along the way, use the finished working example: Kora Kotlin Dependency Injection Introduction App.
What You'll Learn¶
You'll learn the fundamental concepts of dependency injection and understand:
- Core DI Concepts: What dependency injection is and why it matters
- Kora's Architecture: How compile-time DI works and its advantages
- Component Lifecycle: How components are created, managed, and destroyed
- Module System: How to organize and structure your application components
- Best Practices: Patterns for writing maintainable, testable code
What You'll Need¶
- JDK 17 or later
- Gradle 7+
- A text editor or IDE
- Basic understanding of Java or Kotlin
Prerequisites¶
No Prerequisites Required
This guide is designed for beginners and does not require prior knowledge of dependency injection or Kora.
You only need basic Java or Kotlin familiarity, because the guide introduces Kora dependency injection concepts from first principles before showing framework-specific patterns.
Overview¶
Dependency injection is a way to assemble an application from explicit dependencies instead of letting objects create everything they need by themselves. A dependency is simply "something this class needs in order to work": a repository, a client, a configuration object, a cache, a clock, or another service.
For a tiny program, it is natural to write new everywhere. A controller can create a service, the service can create a repository, and the repository can create whatever it needs. But as soon as the
program grows, this becomes hard to maintain:
- classes know too much about how other classes are built
- tests become hard because dependencies are created inside the class
- replacing one implementation requires editing many places
- startup logic spreads across the codebase
- configuration and infrastructure details leak into business code
Dependency injection fixes this by changing the rule: a class should not build its own collaborators. It should declare what it needs, usually through a constructor, and let the application graph provide those objects.
Small Example¶
Without DI, a service might create its repository directly:
public final class UserService {
private final UserRepository repository = new InMemoryUserRepository();
}
That looks simple, but UserService is now tied to one repository implementation. A test cannot easily replace it. A future database repository cannot be plugged in without editing the service.
With constructor injection, the service only declares the dependency:
public final class UserService {
private final UserRepository repository;
public UserService(UserRepository repository) {
this.repository = repository;
}
}
Now UserService does not care whether the repository is in-memory, JDBC-backed, mocked in a test, or wrapped with caching. That decision moves to the application graph.
Object Graphs¶
An application is not just a pile of classes. It is a graph of objects connected by dependencies. For example:
This is called a dependency graph or object graph. Each arrow means "this object needs that object". Kora's main job is to build this graph correctly, start lifecycle-aware components in the right order, and fail the build when the graph cannot be assembled.
Thinking in graphs is one of the most important Kora concepts. When you add a controller, repository, HTTP client, cache, or configuration object, you are adding a node or edge to the graph.
Inversion of Control¶
The deeper idea behind dependency injection is inversion of control. Instead of a service deciding how to construct its repository, client, cache, or configuration, it only declares that it needs them. Object creation moves out of the service and into the application graph.
That changes the shape of application code:
- constructors describe required collaborators
- interfaces make replacement points explicit
- tests can provide mocks or alternate implementations
- startup wiring becomes a separate concern from business logic
Dependency Injection with Kora¶
Kora's compile-time container implements dependency injection at compile time. The @KoraApp interface marks the graph root, @Component marks graph-managed classes,
and @Module contributes factories or framework capabilities. During compilation, Kora analyzes the graph and generates code that creates and connects components.
This gives Kora a different failure model from runtime DI frameworks. Missing dependencies, ambiguous bindings, and some lifecycle problems can be reported during the build rather than during application startup.
For beginners, the most important annotations are:
@KoraApp: the root of the application graph@Component: a class Kora can create automatically@Module: a collection of component factories or imported framework modules
You can think of @KoraApp as the map of the application, @Component as a graph node, and constructor parameters as arrows between nodes.
Compile-Time Injection¶
Compile-time DI means Kora checks and generates wiring during the build. That matters because many DI mistakes are structural mistakes:
- a required dependency has no provider
- two providers match the same dependency and Kora cannot choose
- a module was not imported into the application
- a component depends on another component that cannot be built
In a runtime DI framework, some of these errors may appear only when the app starts. In Kora, the build can fail earlier, before the application is packaged or deployed. This makes feedback faster and keeps production startup more predictable.
Discovery Scope¶
Kora does not blindly scan every class on the classpath. Components are discovered in Gradle modules that contain @KoraApp or @KoraSubmodule interfaces. Components from external libraries are also
not automatically available just because they exist in a JAR. A library normally exposes a module interface, and your application imports that module by extending it from @KoraApp.
This explicitness is important: it keeps the graph predictable, makes module boundaries visible, and avoids accidental component registration.
The practical learning flow is:
- understand why manual object creation becomes painful
- learn what a dependency is
- introduce constructor injection
- connect dependency injection to object graphs and inversion of control
- compare runtime DI with Kora's compile-time graph
- learn how Kora discovers components and modules
- see why generated graph code improves wiring feedback
DI Basics¶
This guide provides a comprehensive introduction to dependency injection (DI) and inversion of control (IoC) principles using the Kora framework. Whether you're new to these concepts or looking to deepen your understanding, this section will systematically build your knowledge from fundamental principles to practical implementation.
What Is Dependency Injection?¶
Dependency Injection is a fundamental design pattern that addresses how software components acquire and manage their dependencies. At its core, DI is about separating the creation of dependencies from their usage, allowing for more flexible and maintainable code architecture.
Core Concept: Instead of a component creating its own dependencies, those dependencies are provided (injected) from an external source. This external source is typically a dependency injection framework or container.
Basic Example:
// Traditional approach - component creates its own dependencies
public class OrderProcessor {
private Database database = new Database(); // Component creates dependency
private EmailService emailService = new EmailService();
public void processOrder(Order order) {
database.save(order);
emailService.sendConfirmation(order.getCustomerEmail());
}
}
// Dependency injection approach - dependencies are provided
public class OrderProcessor {
private final Database database;
private final EmailService emailService;
// Dependencies are injected through constructor
public OrderProcessor(Database database, EmailService emailService) {
this.database = database;
this.emailService = emailService;
}
public void processOrder(Order order) {
database.save(order);
emailService.sendConfirmation(order.getCustomerEmail());
}
}
// Traditional approach - component creates its own dependencies
class OrderProcessor {
private val database = Database() // Component creates dependency
private val emailService = EmailService()
fun processOrder(order: Order) {
database.save(order)
emailService.sendConfirmation(order.customerEmail)
}
}
// Dependency injection approach - dependencies are provided
class OrderProcessor(
private val database: Database,
private val emailService: EmailService
) {
// Dependencies are injected through primary constructor
fun processOrder(order: Order) {
database.save(order)
emailService.sendConfirmation(order.customerEmail)
}
}
Key Terminology:
- Dependency: Any object or service that a component requires to function
- Injection: The process of providing dependencies to a component
- Injector/Container: The mechanism responsible for creating and injecting dependencies
Traditional Approach Problems¶
To understand the necessity of dependency injection, let's examine the challenges that arise without it and how DI provides solutions.
The Problem: Tight Coupling
Tight coupling occurs when components are directly dependent on specific implementations, making the system rigid and difficult to maintain. Consider this common pattern:
Problems with Tight Coupling:
- Testing Difficulties: The
UserServicecannot be tested in isolation because it directly instantiatesDatabaseConnection - Implementation Lock-in: Changing to a different database requires modifying the
UserServicecode - Hidden Dependencies: The constructor reveals nothing about what the service actually needs
- Resource Management Issues: Each instance creates its own database connection
- Configuration Problems: No way to configure the database connection externally
Dependency Injection Benefits¶
The Dependency Injection Solution:
public class UserService {
private final DatabaseConnection connection;
// Dependencies are explicitly declared
public UserService(DatabaseConnection connection) {
this.connection = connection;
}
public User findUserById(long id) {
return connection.query("SELECT * FROM users WHERE id = ?", id);
}
}
Key Benefits of Dependency Injection:
- Testability: Components can be tested with mock dependencies
- Flexibility: Different implementations can be injected based on environment
- Explicit Dependencies: Constructor parameters clearly document requirements
- Resource Management: Connection lifecycle can be managed externally
- Configuration: Database settings can be configured at the application level
Understanding Inversion of Control¶
Inversion of Control is the architectural principle that underlies dependency injection. IoC represents a fundamental shift in how control flow is managed in software systems.
Traditional Control Flow:
Inverted Control Flow:
Framework/Container -> Creates Objects -> Injects Dependencies -> Application Code Executes Business Logic
The Inversion Principle:
In traditional programming, your application code is responsible for:
- Creating all necessary objects
- Managing object lifecycles
- Coordinating between components
- Handling configuration
With IoC, these responsibilities are inverted:
- The framework creates objects
- The framework manages lifecycles
- The framework coordinates components
- The framework handles configuration
IoC Implementation Patterns:
- Factory Pattern: Centralized object creation
- Service Locator: Components request dependencies from a central registry
- Dependency Injection: Dependencies are pushed into components
Why IoC Matters:
IoC enables several important architectural benefits:
- Separation of Concerns: Business logic is separated from infrastructure concerns
- Modularity: Components can be developed and tested independently
- Maintainability: Changes to infrastructure don't affect business logic
- Testability: Components can be easily isolated for testing
- IoC: Restaurant provides ready-made meals, you just eat
In Code:
// Traditional approach - you control all object creation
public class Application {
public static void main(String[] args) {
Database db = new Database(); // You create
EmailService email = new EmailService(); // You create
OrderService service = new OrderService(db, email); // You create
service.processOrder(order); // You control
}
}
When Old Approaches Break¶
While the traditional approach of manually creating and managing dependencies works perfectly well for small applications with just a few classes, it becomes increasingly problematic as your application grows to dozens or hundreds of components.
Why Scale Matters:
The traditional approach requires you to manually instantiate and wire together every object in your application. For a small app with 3-5 classes, this is straightforward. But when your application contains 20, 50, or 100+ classes, this manual approach becomes a maintenance nightmare.
Example: A 20+ Class Application (Traditional Approach)
Imagine building an application with the following components:
public class EcommerceApplication {
public static void main(String[] args) {
// Infrastructure Layer (8 classes)
DatabaseConfig dbConfig = new DatabaseConfig("localhost", "ecommerce", "user", "pass");
DatabaseConnection dbConnection = new DatabaseConnection(dbConfig);
RedisConfig redisConfig = new RedisConfig("localhost", 6379);
RedisConnection redisConnection = new RedisConnection(redisConfig);
EmailConfig emailConfig = new EmailConfig("smtp.gmail.com", 587, "user@gmail.com");
EmailService emailService = new EmailService(emailConfig);
PaymentGatewayConfig paymentConfig = new PaymentGatewayConfig("stripe_key_123");
PaymentGateway paymentGateway = new PaymentGateway(paymentConfig);
// Data Access Layer (6 classes)
UserRepository userRepository = new UserRepository(dbConnection);
ProductRepository productRepository = new ProductRepository(dbConnection);
OrderRepository orderRepository = new OrderRepository(dbConnection);
CartRepository cartRepository = new CartRepository(redisConnection);
AuditRepository auditRepository = new AuditRepository(dbConnection);
InventoryRepository inventoryRepository = new InventoryRepository(dbConnection);
// Business Logic Layer (8 classes)
UserService userService = new UserService(userRepository, emailService);
ProductService productService = new ProductService(productRepository, inventoryRepository);
CartService cartService = new CartService(cartRepository, productService);
OrderService orderService = new OrderService(orderRepository, paymentGateway, emailService);
PaymentService paymentService = new PaymentService(paymentGateway, orderRepository);
InventoryService inventoryService = new InventoryService(inventoryRepository, productRepository);
AuditService auditService = new AuditService(auditRepository);
NotificationService notificationService = new NotificationService(emailService);
// Presentation Layer (4 classes)
UserController userController = new UserController(userService, auditService);
ProductController productController = new ProductController(productService, auditService);
OrderController orderController = new OrderController(orderService, cartService, auditService);
CartController cartController = new CartController(cartService, auditService);
// Application Bootstrap (2 classes)
// ... and more
}
}
class EcommerceApplication {
companion object {
@JvmStatic
fun main() {
// Infrastructure Layer (8 classes)
val dbConfig = DatabaseConfig("localhost", "ecommerce", "user", "pass")
val dbConnection = DatabaseConnection(dbConfig)
val redisConfig = RedisConfig("localhost", 6379)
val redisConnection = RedisConnection(redisConfig)
val emailConfig = EmailConfig("smtp.gmail.com", 587, "user@gmail.com")
val emailService = EmailService(emailConfig)
val paymentConfig = PaymentGatewayConfig("stripe_key_123")
val paymentGateway = PaymentGateway(paymentConfig)
// Data Access Layer (6 classes)
val userRepository = UserRepository(dbConnection)
val productRepository = ProductRepository(dbConnection)
val orderRepository = OrderRepository(dbConnection)
val cartRepository = CartRepository(redisConnection)
val auditRepository = AuditRepository(dbConnection)
val inventoryRepository = InventoryRepository(dbConnection)
// Business Logic Layer (8 classes)
val userService = UserService(userRepository, emailService)
val productService = ProductService(productRepository, inventoryRepository)
val cartService = CartService(cartRepository, productService)
val orderService = OrderService(orderRepository, paymentGateway, emailService)
val paymentService = PaymentService(paymentGateway, orderRepository)
val inventoryService = InventoryService(inventoryRepository, productRepository)
val auditService = AuditService(auditRepository)
val notificationService = NotificationService(emailService)
// Presentation Layer (4 classes)
val userController = UserController(userService, auditService)
val productController = ProductController(productService, auditService)
val orderController = OrderController(orderService, cartService, auditService)
val cartController = CartController(cartService, auditService)
// Application Bootstrap (2 classes)
// ... and more
}
}
}
With 100+ Classes, This Becomes Impossible:
- Your main method would be 1000+ lines long
- Understanding the dependency graph requires a separate diagram
- You must manually ensure components are created in the correct order
- Adding a new feature requires touching dozens of files
- A change to one component requires understanding its entire dependency chain
- Testing any component requires instantiating hundreds of objects and becomes nightmare
- A single configuration change cascades through the entire application
- Adding a new feature requires updating the main method, potentially breaking existing initialization order
The Dependency Injection Solution:
With DI, you declare dependencies at the component level, and the framework handles all the complexity:
@KoraApp
public interface EcommerceApplication extends
InfrastructureModule, DataAccessModule, BusinessLogicModule, PresentationModule {
static void main(String[] args) {
KoraApplication.run(EcommerceApplicationGraph::graph);
}
}
// Each component just declares what it needs
@Component
public final class OrderService {
private final OrderRepository orderRepository;
private final PaymentGateway paymentGateway;
private final EmailService emailService;
public OrderService(OrderRepository orderRepository,
PaymentGateway paymentGateway,
EmailService emailService) {
this.orderRepository = orderRepository;
this.paymentGateway = paymentGateway;
this.emailService = emailService;
}
}
@KoraApp
interface EcommerceApplication :
InfrastructureModule, DataAccessModule, BusinessLogicModule, PresentationModule
fun main() {
KoraApplication.run(EcommerceApplicationGraph::graph)
}
// Each component just declares what it needs
@Component
class OrderService(
private val orderRepository: OrderRepository,
private val paymentGateway: PaymentGateway,
private val emailService: EmailService
)
The framework automatically:
- Creates all objects in the correct order
- Manages resource lifecycles
- Handles configuration injection
- Provides dependency resolution
- Enables easy testing with mocks
This is why dependency injection becomes essential as applications grow beyond a handful of classes.
Benefits Comparison:
| Aspect | Traditional | Dependency Injection |
|---|---|---|
| Testing | Hard (uses real services) | Easy (inject mocks) |
| Flexibility | Low (hardcoded dependencies) | High (inject any implementation) |
| Reusability | Low (tied to specific implementations) | High (works with any compatible service) |
| Maintainability | Low (changes affect multiple places) | High (change injection, not code) |
| Clarity | Low (dependencies hidden) | High (constructor shows needs) |
Now that you understand the fundamentals, let's explore how Kora implements these concepts with compile-time dependency injection!
Kora Architecture¶
Kora uses compile-time dependency injection, which means:
- Build-time Analysis: Dependencies are analyzed during compilation using annotation processors
- Component Discovery: Classes annotated with
@Componentand factory methods are found - Dependency Resolution: The annotation processor resolves all dependencies and builds a dependency graph
- Code Generation: An
ApplicationGraphDrawclass is generated as Java/Kotlin source code - Runtime Performance: No reflection or runtime analysis overhead - everything is resolved at compile time
Important Scope Limitation: Kora's annotation processors only scan Gradle modules that contain
@KoraAppor@KoraSubmoduleinterfaces. Components in regular Gradle modules without these interfaces will not be discovered or processed by the DI system.
How It Works in Kora¶
- Annotation Processing:
@KoraAppinterfaces are processed at compile time byKoraAppProcessor - Component Discovery: Scans for
@Componentclasses,@Moduleinterfaces, and factory methods within Gradle modules containing@KoraAppor@KoraSubmoduleinterfaces - Dependency Resolution: Uses
GraphBuilderto resolve dependencies and detect cycles - Graph Generation: Generates
ApplicationGraphclass with component factories and initialization logic - Runtime Execution:
KoraApplication.run()initializes components in correct order
Critical Scope Limitation: Kora's annotation processors only process Gradle modules that contain
@KoraAppor@KoraSubmoduleinterfaces. Components in regular Gradle modules without these interfaces will be completely ignored by the DI system.
Architectural Benefits of Explicit Control: This deliberate design choice gives you complete control over your application's dependency graph. Unlike frameworks that automatically instantiate everything on the classpath, Kora ensures you explicitly declare what components you want. This prevents:
- Resource waste from unwanted component instantiation
- Security risks from transitive dependency components being activated
- Debugging complexity from unknown running components
- Performance overhead from classpath scanning
- Unpredictable behavior when dependencies change
With Kora, your @KoraApp interface serves as an explicit manifest of everything running in your application.
Generated Code¶
When you annotate an interface with @KoraApp, Kora generates:
Compile Time and Runtime¶
Compile Time (Annotation Processing):
- Analyzes source code for components and dependencies within
@KoraApp/@KoraSubmodulemodules only - Validates dependency graph (no cycles, all dependencies available)
- Generates optimized initialization code
- Provides compile-time error checking
Runtime (Application Execution):
- Executes generated initialization code
- Manages component lifecycle
- Handles graceful shutdown
- Supports component updates via
ValueOf<T>
Scope Critical: Compile-time processing only occurs in Gradle modules containing
@KoraAppor@KoraSubmoduleinterfaces. Code in regular modules is not analyzed or processed at compile time.
Annotation Processors¶
Kora's annotation processing consists of:
- KoraAppProcessor: Main processor handling
@KoraApp,@Module,@Component - GraphBuilder: Builds dependency resolution graph and detects cycles
- ComponentDependencyHelper: Parses dependency claims from method/constructor parameters
- Extensions: Pluggable system for generating components dynamically
- ProcessingContext: Provides access to compilation environment and utilities
Scope Limitation: Kora's annotation processors only activate and process code within Gradle modules that contain
@KoraAppor@KoraSubmoduleinterfaces. Code in regular Gradle modules is completely invisible to these processors.
Component Discovery Order¶
Components are discovered in this priority order (higher numbers override lower):
- Auto Creation: Classes meeting requirements (final, single constructor, no abstract)
- Extension Mechanism: Dynamic component generation (JSON mappers, repositories, etc.)
- Generic Factory: Methods with generic parameters
- Standard Factory: Methods with
@DefaultComponent - Basic Factory: Regular factory methods
- Module Factory: Methods in
@Moduleinterfaces - External Module Factory: Inherited from external dependencies
- Submodule Factory: Generated from
@KoraSubmodule - Auto Factory: Classes with
@Componentannotation
Scope Note: Component discovery only occurs within Gradle modules containing
@KoraAppor@KoraSubmoduleinterfaces. Components in regular Gradle modules will not be discovered regardless of their annotations.
Dependency Resolution Algorithm¶
- Claim Parsing: Each dependency parameter is parsed into a
DependencyClaim - Component Matching: Find components matching type and tags
- Cycle Detection: Ensure no circular dependencies exist
- Graph Construction: Build acyclic dependency graph
- Code Generation: Generate initialization code in topological order
Core Annotations¶
Kora provides several key annotations for dependency injection:
@KoraApp¶
Marks the main application interface and serves as the core of Kora's dependency container. This annotation labels the interface within which factory methods for creating components and module dependencies are defined. There can be only one such interface within an application.
What @KoraApp Does:
- Container Entry Point: Defines the root of your application's dependency container
- Component Registry: Registers all factory methods and component accessors
- Module Integration: Connects external modules through interface inheritance
- Application Bootstrap: Provides the starting point for
KoraApplication.run()
Requirements:
- Must be an interface (not a class)
- Only one per application
- Can extend multiple module interfaces
- Must be in a Gradle module (not regular modules without @KoraSubmodule)
Container Building Process:
At compile time, Kora uses the @KoraApp interface to:
- Discover all factory methods and component dependencies
- Validate the dependency graph for cycles and missing components
- Generate optimized initialization code
- Create the
ApplicationGraphclass for runtime execution
Why Interfaces? Multiple Inheritance and Factory Override Control
Kora requires @KoraApp and all modules to be interfaces rather than classes for fundamental architectural reasons that enable powerful dependency injection capabilities.
Why Interfaces? Multiple Inheritance and Factory Override Control
Kora requires @KoraApp and all modules to be interfaces rather than classes for fundamental architectural reasons that enable powerful dependency injection capabilities.
Multiple Inheritance: Java interfaces support multiple inheritance, allowing your application to compose functionality from multiple modules:
Factory Method Override: Interface default methods can be easily overridden, giving you complete control over dependency injection at the language level:
// Library provides default implementation
@Module
public interface CacheModule {
@DefaultComponent
default Cache cache() {
return new InMemoryCache(); // Default implementation
}
}
// Your application can override with custom implementation
@KoraApp
public interface Application extends CacheModule { // <----- Connected module
@Override
default Cache cache() {
return new RedisCache(); // Override with Redis
}
}
// Library provides default implementation
@Module
interface CacheModule {
@DefaultComponent
fun cache(): Cache = InMemoryCache() // Default implementation
}
// Your application can override with custom implementation
@KoraApp
interface Application : CacheModule { // <----- Connected module
override fun cache(): Cache = RedisCache() // Override with Redis
}
Component as Factory Method: Components aren't limited to classes - they can also be defined as factory methods in interfaces, giving you declarative control over IoC:
@KoraApp
public interface Application {
// Component defined as factory method (not a class)
default UserService userService(UserRepository repository, EmailService email) {
// You control exactly how UserService is created
var service = new UserService(repository, email);
service.setTimeout(Duration.ofSeconds(30)); // Custom configuration
return service;
}
// Another component as factory method
default OrderProcessor orderProcessor(UserService userService, PaymentService payment) {
return new OrderProcessor(userService, payment, new OrderValidator());
}
}
@KoraApp
interface Application {
// Component defined as factory method (not a class)
fun userService(repository: UserRepository, email: EmailService): UserService {
// You control exactly how UserService is created
val service = UserService(repository, email)
service.setTimeout(Duration.ofSeconds(30)) // Custom configuration
return service
}
// Another component as factory method
fun orderProcessor(userService: UserService, payment: PaymentService): OrderProcessor =
OrderProcessor(userService, payment, OrderValidator())
}
Why This Design Matters:
- Intuitive Language-Level Control: IoC behavior is controlled using familiar Java language constructs (interfaces, default methods) rather than complex XML/annotations
- Type-Safe Configuration: Factory methods are checked at compile-time, preventing runtime configuration errors
- Easy Testing: Factory methods can be overridden in tests to inject mocks without complex test frameworks
- Modular Composition: Multiple inheritance allows clean separation of concerns across different modules
- Override Flexibility: Change implementations by simply overriding methods, no framework-specific configuration needed
This interface-based approach makes dependency injection feel like a natural extension of the Java language, giving you powerful IoC capabilities while maintaining simplicity and type safety.
Why Explicit Control Matters¶
Kora's design philosophy prioritizes explicit control over implicit magic. Unlike traditional DI frameworks that automatically scan the classpath and instantiate everything they find, Kora requires you to explicitly declare what dependencies you want in your application.
The Problem with Automatic Discovery:
- Unpredictable Behavior: You never know what will be instantiated just by adding a JAR to your classpath
- Hidden Dependencies: Components can be created without your knowledge, consuming resources
- Debugging Nightmares: When something goes wrong, you have to figure out what unwanted components are running
- Security Risks: Malicious or vulnerable components might be instantiated automatically
- Performance Issues: Every JAR on the classpath gets scanned, even if not needed
Kora's Explicit Approach:
Benefits of Explicit Control:
- Predictable Dependencies: You know exactly what's running in your application
- Resource Efficiency: Only instantiate what you actually need
- Clear Dependency Graph: Easy to understand and debug component relationships
- Security by Design: No surprise instantiations from transitive dependencies
- Performance: No classpath scanning overhead - everything is resolved at compile time
- Maintainability: Changes to dependencies are explicit and tracked in code
Real-World Impact:
With automatic frameworks, developers often spend hours debugging why their application is slow or consuming unexpected resources. With Kora, if a component isn't explicitly included in
your @KoraApp interface, it simply doesn't exist in your application - no surprises, no hidden costs.
@Component¶
Marks a class as a component (dependency) in the dependency container. All components in Kora are singletons - classes that have only one instance created throughout the application lifecycle.
Components are injected only if they are root components (marked with @Root) or if they are required as dependencies by other components.
What Components Are:
- Singleton Instances: One instance per application lifecycle
- Dependency Providers: Can be injected into other components
- Conditional Initialization: Created only if required by other components or marked with
@Root - Thread-Safe: Same instance shared across all injection points
Important Scope Limitation: @Component classes can only be discovered and used within Gradle modules that contain either:
- A
@KoraAppinterface (main application module) - A
@KoraSubmoduleinterface (component discovery module)
Components in regular Gradle modules without these annotations will not be processed by Kora's annotation processor.
Requirements for Auto Factory:
- Class must not be abstract
- Must have exactly one public constructor
- Must be
final(unless it has AOP aspects) - Constructor parameters become dependencies
- Must be in a Gradle module with @KoraApp or @KoraSubmodule
Component Lifecycle:
- Discovery: Found by annotation processor during compilation
- Validation: Dependencies checked at compile time
- Creation: Instance created at application startup if required (or marked with
@Root) - Injection: Same instance provided to all dependent components
- Destruction: Managed by container during shutdown
@Module¶
Groups related component factories together and marks interfaces as modules to be injected into the dependency container at compile time. A module is an interface that contains factory methods for creating components. All factory methods within a module become available to the dependency container.
What Modules Do:
- Factory Collection: Group related component factories in one place
- Code Organization: Separate concerns across different modules
- Reusability: Modules can be shared across applications
- Override Support: Factory methods can be overridden in extending interfaces
Scope: @Module interfaces are processed within Gradle modules that contain @KoraApp or @KoraSubmodule interfaces. External modules from libraries are inherited through interface extension.
Module Types:
- Internal Modules: Defined in your project within
@KoraAppmodules - External Modules: Provided by libraries (inherited via interface extension)
- Submodules: Generated from
@KoraSubmoduleinterfaces
Module Requirements:
- Must be an interface (not a class)
- Factory methods must be
defaultmethods - Must be in the same source directory as
@KoraAppor@KoraSubmodule
Factory Method Rules:
- Must return a component (non-null value)
- Can take other components as parameters
- Parameters become dependencies
- Parameters mey be optional components (mark
@Nullable) - Methods are called in dependency order at runtime
External Library Components: Components and modules from external libraries are not automatically discovered by Kora's annotation processor. Even if a library contains
@Componentclasses or@Moduleinterfaces, they will be invisible to your application unless you explicitly extend their module interfaces in your@KoraAppinterface. This is a deliberate design choice for explicit dependency management.
@KoraSubmodule¶
Marks an interface for which to build a module for the current compilation module. It will contain all components marked with @Module and @Component annotations found in the source code. This
annotation is particularly useful for multi-module Gradle applications where different modules contain different pieces of functionality, and the main @KoraApp application is built in a separate
module.
What @KoraSubmodule Does:
- Component Discovery: Scans the current Gradle module for
@Moduleand@Componentannotations - Module Generation: Creates an inheritor interface with all discovered modules and components
- Multi-Module Support: Enables component sharing across Gradle modules
- Boundary Definition: Defines where Kora's annotation processor scans for components
- Build Optimization: Enables Gradle's build caching and incremental compilation by isolating functionality into separate modules
Scope: @KoraSubmodule interfaces define the boundaries where Kora's annotation processor will scan for components. Components outside these boundaries are not processed.
How It Works:
- Discovery: Finds all
@Moduleinterfaces and@Componentclasses in the current Gradle module - Inheritance: Generated interface inherits from all discovered
@Moduleinterfaces - Factory Generation: Creates default methods for all discovered
@Componentclasses - Integration: Can be extended by
@KoraAppto include components from other modules
Use Cases:
- Multi-Module Projects: Share components across Gradle modules
- Library Development: Expose components from a library module
- Modular Architecture: Separate concerns across different build modules
- Component Organization: Group related components by functionality
- Large Single Applications: Organize complex monolithic applications into isolated Gradle modules for better build performance and maintainability
- Build Optimization: Leverage Gradle's build caching context by separating functionality into independent modules that can be built and cached separately
@Root¶
Marks components that should always be initialized with application startup, even if they are not dependencies of other components. Root components are guaranteed to be created and started when the application launches, regardless of whether anything injects them.
What @Root Does:
- Guaranteed Initialization: Component is always created at startup
- Eager Loading: Forces immediate instantiation (not lazy)
- Lifecycle Management: Component participates in application startup/shutdown
- Entry Points: Perfect for servers, consumers, schedulers, and background services
Common Use Cases:
- HTTP Servers: Web servers that need to start listening immediately
- Message Consumers: Kafka consumers, queue processors
- Background Services: Cache warmers, health checkers, schedulers
@Root vs Regular Components:
- Regular Components: Created only if required as dependencies by other components
- @Root Components: Always created at startup (guaranteed initialization)
When to Use @Root:
- Component provides a service that should always be running
- Component needs to start processing immediately (servers, consumers)
- Component performs critical initialization (database setup, cache warming)
- Component collects metrics or monitoring data
@DefaultComponent¶
Marks factory methods that provide default implementations, which are intended to be overridden by users. If any component is found in the dependency container without this annotation, it will take
precedence during injection over @DefaultComponent factories.
What @DefaultComponent Does:
- Default Provision: Provides fallback implementations for components
- Override Support: Allows users to replace defaults without modifying library code
- Library-Friendly: Enables libraries to provide sensible defaults
- Priority System: Lower priority than non-annotated factories
Use Cases:
- Library Defaults: Libraries provide default implementations that users can override
- Configuration Options: Different implementations based on environment
- Extension Points: Allow users to customize behavior without changing library code
Override Behavior:
Priority Order:
- Non-annotated factories (highest priority - overrides defaults)
@DefaultComponentfactories (lowest priority - can be overridden)- Other factory types in between
Best Practices:
- Use for library-provided defaults that users might want to customize
- Don't use for application-specific components
- Clearly document what defaults are available for override
@Tag¶
Allows differentiation of multiple implementations of the same type and provides selective injection based on tags. Tags use class references instead of strings for better refactoring support and type safety. A component is registered with a specific tag and injected at points that request exactly the same tag.
What Tags Do:
- Implementation Selection: Choose specific implementations of interfaces
- Multiple Instances: Support multiple implementations of the same type
- Type Safety: Uses class references instead of strings
- Refactoring Safe: IDE can track tag usage across codebase
Basic Usage:
// Tag classes (usually empty marker classes)
public final class RedisTag {}
public final class InMemoryTag {}
// Tagged implementations
@Tag(RedisTag.class)
@Component
public final class RedisCache implements Cache {
// Redis implementation
}
@Tag(InMemoryTag.class)
@Component
public final class InMemoryCache implements Cache {
// In-memory implementation
}
// Selective injection
@Component
public final class UserService {
public UserService(@Tag(RedisTag.class) Cache cache) {
// Injects RedisCache specifically
}
}
// Tag classes (usually empty marker classes)
class RedisTag
class InMemoryTag
// Tagged implementations
@Tag(RedisTag::class)
@Component
class RedisCache : Cache {
// Redis implementation
}
@Tag(InMemoryTag::class)
@Component
class InMemoryCache : Cache {
// In-memory implementation
}
// Selective injection
@Component
class UserService(@Tag(RedisTag::class) private val cache: Cache) {
// Injects RedisCache specifically
}
Tag Application:
- On Classes:
@Tag(MyTag.class) @Component class MyClass - On Factory Methods:
@Tag(MyTag.class) default MyClass myClass() - On Parameters:
public MyClass(@Tag(MyTag.class) Dependency dep)
Special Tags:
@Tag.Any: Matches all components regardless of their tags- Custom tag annotations can be created for convenience
Tag Matching Rules:
- Exact Match: Tags must match exactly by class reference
- Inheritance: Tag classes can be part of inheritance hierarchies
- Multiple Tags: Components can have multiple tags
- Tag Filtering: Dependencies can specify required tags
Custom Tag Annotations:
@Tag(RedisTag.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
public @interface RedisCache {}
@Tag(InMemoryTag.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
public @interface InMemoryCache {}
// Usage
@RedisCache
@Component
public final class RedisCacheImpl implements Cache {}
@Component
public final class UserService {
public UserService(@RedisCache Cache cache) {/* ... */}
}
@Tag(RedisTag::class)
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER)
annotation class RedisCache
@Tag(InMemoryTag::class)
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER)
annotation class InMemoryCache
// Usage
@RedisCache
@Component
class RedisCacheImpl : Cache
@Component
class UserService(@RedisCache private val cache: Cache)
Component Discovery Priority¶
When Kora needs to create a component, it follows a specific priority order to determine which factory method or mechanism to use. Higher priority factories override lower priority ones. Understanding this order is crucial for debugging dependency resolution issues and ensuring the correct implementations are used.
Priority Order (Highest to Lowest):
- Auto Creation: Classes meeting component requirements (final, single constructor, no abstract)
- Extension Mechanism: Dynamic component generation (JSON mappers, repositories, etc.)
- Generic Factory: Methods with generic type parameters
- Standard Factory: Methods with
@DefaultComponent - Basic Factory: Regular factory methods
- Module Factory: Methods in
@Moduleinterfaces - External Module Factory: Inherited from external dependencies
- Submodule Factory: Generated from
@KoraSubmodule - Auto Factory: Classes with
@Componentannotation
What This Means:
- If you have both a
@Componentclass and a factory method for the same type, the factory method takes precedence @DefaultComponentfactories can be overridden by regular factory methods- Extensions can provide components dynamically (like JSON readers/writers)
- Auto creation works as a fallback for simple classes
Practical Example:
// Priority 9: Auto Factory (@Component) - lowest priority
@Component
public final class DefaultUserService implements UserService { }
// Priority 5: Basic Factory - higher priority, overrides @Component
@KoraApp
public interface Application {
default UserService userService() {
return new CustomUserService(); // This will be used instead
}
}
Declaring Components¶
Components in Kora can be declared in multiple ways, each with different priorities and use cases. All component declaration methods require the code to be within Gradle modules that
contain @KoraApp or @KoraSubmodule interfaces - Kora's annotation processor only scans these designated modules.
Automatic Factory (@Component)¶
Classes annotated with @Component are automatically registered if they meet the requirements:
Requirements:
- Not abstract
- Exactly one public constructor
- Final class (unless AOP aspects applied)
- Constructor parameters become dependencies
Basic Factory Methods¶
Default methods in @KoraApp or @Module interfaces that return components:
Module Factory¶
Factory methods within @Module interfaces:
External Module Factory¶
Modules from external dependencies, inherited through interface extension:
Explicit Import Required: External library components are not automatically available. You must explicitly extend the library's module interfaces in your
@KoraAppinterface. Simply adding a library to your classpath is not enough - the module interface extension makes the components available for dependency injection.
This explicit approach prevents the common problems of automatic frameworks:
- No surprise instantiation of unwanted components
- Clear visibility into what dependencies are actually used
- Better security through intentional inclusion
- Easier debugging and maintenance
Submodule Factory¶
Generated modules from @KoraSubmodule interfaces:
@Module
public interface PersistenceModule {
default UserRepository userRepository() {
return new InMemoryUserRepository();
}
}
@KoraSubmodule
public interface ApplicationSubmodule {
// Generates factory methods for all @Module and @Component in the project
}
@KoraApp
public interface Application extends ApplicationSubmodule { // <----- Connected module
// All components from submodules are available
}
@Module
interface PersistenceModule {
fun userRepository(): UserRepository =
InMemoryUserRepository()
}
@KoraSubmodule
interface ApplicationSubmodule {
// Generates factory methods for all @Module and @Component in the project
}
@KoraApp
interface Application : ApplicationSubmodule { // <----- Connected module
// All components from submodules are available
}
Generic Factory¶
Methods with generic type parameters that can create components of any matching type. Generic factories are particularly useful for creating type-safe components that work with different generic types.
public interface ValidatorModule {
// Generic factory for List validators
default <T> Validator<List<T>> listValidator(Validator<T> validator, TypeRef<T> valueRef) {
return new IterableValidator<>(validator);
}
// Generic factory for Set validators
default <T> Validator<Set<T>> setValidator(Validator<T> validator, TypeRef<T> valueRef) {
return new IterableValidator<>(validator);
}
// Generic factory for Collection validators
default <T> Validator<Collection<T>> collectionValidator(Validator<T> validator, TypeRef<T> valueRef) {
return new IterableValidator<>(validator);
}
}
interface ValidatorModule {
// Generic factory for List validators
fun <T> listValidator(validator: Validator<T>, valueRef: TypeRef<T>): Validator<List<T>> =
IterableValidator(validator)
// Generic factory for Set validators
fun <T> setValidator(validator: Validator<T>, valueRef: TypeRef<T>): Validator<Set<T>> =
IterableValidator(validator)
// Generic factory for Collection validators
fun <T> collectionValidator(validator: Validator<T>, valueRef: TypeRef<T>): Validator<Collection<T>> =
IterableValidator(validator)
}
How It Works:
- The
<T>type parameter allows creating validators for any element type TypeRef<T>provides runtime type information for generic operations- Can create
Validator<List<String>>,Validator<Set<User>>, etc. - Enables type-safe validation of generic collections
Extension Mechanism¶
Components generated dynamically by extensions (JSON mappers, repositories, etc.):
// Extensions automatically generate components for:
-JSON readers/writers for classes
-
Database repositories
from interfaces
-
HTTP clients
from interfaces
-
And many
more...
@DefaultComponent Factory¶
Default implementations that can be overridden:
@Module
public interface CacheModule {
@DefaultComponent
default Cache cache() {
return new InMemoryCache();
}
}
// Can be overridden in application:
@KoraApp
public interface Application extends CacheModule { // <----- Connected module
default Cache primaryCache() {
return new RedisCache(); // Overrides the default
}
}
Automatic Creation¶
Classes that meet component requirements but aren't explicitly annotated:
Priority Order (highest to lowest):
- Auto Creation
- Extension Mechanism
- Generic Factory
- Standard Factory (@DefaultComponent)
- Basic Factory
- Module Factory
- External Module Factory
- Submodule Factory
- Auto Factory (@Component)
Dependency Claims and Resolution¶
Kora uses a sophisticated dependency resolution system based on "claims". Each dependency parameter is parsed into a DependencyClaim that specifies how the dependency should be resolved.
This is the point where constructor parameters stop being just Java or Kotlin types and become graph requirements. Kora looks at the requested type, wrapper type, nullability annotations, and tags, then
decides which component can satisfy that request.
Understanding dependency claims helps you read compiler errors. When Kora says that a dependency is missing, ambiguous, nullable, or cyclic, it is describing the claim it tried to resolve and the component candidates it found in the graph.
Basic Dependency Types¶
Most Kora dependencies are expressed directly in constructors or factory method parameters. The shape of the parameter tells Kora whether the component is required, optional, lazily accessed, or a collection of implementations. These shapes let you model the relationship between components without adding container APIs to your business code.
Use the simplest shape that matches the domain rule. If the service cannot work without a repository, request the repository directly. If an integration is optional, mark it nullable. If you need all
implementations of an extension point, request All<T>. If you want to avoid refresh cascades or delay access to the actual component, request ValueOf<T>.
Required¶
Single required dependency that must exist: This is the default and most common dependency form. A required parameter means the application graph is invalid unless exactly one matching component is available. It is the right choice for core collaborators such as repositories, services, validators, configuration interfaces, and clients that are part of the normal application flow.
Required dependencies make failures explicit. If you forget to import a module or define a component, the build fails while Kora generates the graph instead of letting the application start with a partially configured runtime.
Optional¶
Single optional dependency that may be null:
Nullable dependencies are useful for optional features, optional integrations, or library defaults where the application may provide an extra component but does not have to. Kora still resolves the
dependency by type and tags, but absence is allowed and the generated graph passes null.
Use this deliberately. A nullable dependency should mean "the component can operate without this collaborator", not "I am unsure whether the graph is correct". Business code that receives a nullable dependency should branch explicitly and keep the degraded behavior easy to see.
ValueOf¶
Synchronous access to a component's current value:
ValueOf<T> is a wrapper around a component reference. It lets a component ask for the current value when it needs it instead of holding a direct dependency. This is useful when the dependency may be
refreshed, when initialization should be delayed, or when a direct edge would make the graph update more components than necessary.
In ordinary request-processing code you usually do not need ValueOf<T>. Prefer a direct dependency for simple service collaboration. Reach for ValueOf<T> when the lifecycle behavior matters:
configuration refresh, expensive components, or components that should not force their consumers to refresh at the same time.
Can be also @Nullable synchronous access:
All¶
All implementations of a type as individual dependencies:
All<T> models extension points. Instead of choosing one implementation, Kora injects every matching implementation in a deterministic collection. This is useful for handlers, validators, listeners,
interceptors, exporters, or any place where the application should compose several independent contributions.
The important design point is that every element in All<T> is still a graph component. Kora validates each implementation, applies tags if requested, and wires the collection at compile time. That
keeps plugin-like composition type-safe and visible in the generated graph.
Can also be implementation wrapped in ValueOf:
TypeRef¶
Reference to a type for reflection or generic operations:
TypeRef<T> carries generic type information through type erasure. It is useful when a component needs to know not just the raw class, but the full generic type requested by the graph. JSON mappers,
configuration extractors, serializers, and other generated infrastructure often need this kind of type token.
Most application services do not need to inject TypeRef<T> directly. Treat it as an infrastructure tool for code that creates or adapts components based on generic types. When you do use it, the type
parameter should describe the exact model shape the component is responsible for.
Wrapper Type Contract¶
Wrapper types are Kora's way to express dependency behavior without changing the component being requested. ValueOf<T> says "give me a handle to this component", while All<T> says "give me all
matching components". The wrapped T is still the business type; the wrapper changes how Kora resolves and exposes it.
This distinction keeps APIs readable. A constructor that takes UserRepository needs one repository. A constructor that takes ValueOf<UserRepository> needs controlled access to a repository. A
constructor that takes All<Notifier> needs a collection of notifier implementations. Those signatures document the graph relationship directly in code.
Dependency Resolution Rules¶
Kora resolves dependencies in a predictable order. First it identifies the requested type shape, then applies tags and wrappers, then chooses the highest-priority matching factory or component. If the result is missing, ambiguous, or cyclic, graph generation fails with a compile-time error.
This is why explicit component declarations matter. Adding a dependency to the build file is not enough to make every component in that library appear in the graph. The application must import the right module, define the right component, or request the right tag. The generated graph is the final source of truth for what actually runs.
- Type Matching: Dependencies are matched by type and tags
- Tag Filtering:
@Tagannotations narrow the search - Priority Order: Higher priority factories override lower ones
- Cycle Detection: Circular dependencies are detected at compile time
- Nullability:
@Nullablemarks optional dependencies
Indirect Dependencies¶
Use ValueOf<T> to avoid cascading component refreshes when dependencies get updated:
@Module
public interface ServiceModule {
default ServiceA serviceA() {
return new ServiceA();
}
default ServiceB serviceB() {
return new ServiceB();
}
default ServiceC serviceC(ServiceA serviceA, ValueOf<ServiceB> serviceB) {
// ServiceC depends on ServiceA directly (refreshes cascade to ServiceC)
// ServiceC depends on ServiceB indirectly via ValueOf<T> (prevents cascading refreshes)
return new ServiceC(serviceA, serviceB);
}
}
@Module
interface ServiceModule {
fun serviceA(): ServiceA = ServiceA()
fun serviceB(): ServiceB = ServiceB()
fun serviceC(serviceA: ServiceA, serviceB: ValueOf<ServiceB>): ServiceC {
// ServiceC depends on ServiceA directly (refreshes cascade to ServiceC)
// ServiceC depends on ServiceB indirectly via ValueOf<T> (prevents cascading refreshes)
return ServiceC(serviceA, serviceB)
}
}
Why ValueOfValueOf<T> creates an indirect dependency that prevents this cascading
refresh behavior, allowing components to access updated values without being refreshed themselves.
Tag System¶
Tags allow multiple implementations of the same interface to coexist and be differentiated during dependency injection. Tags use class references instead of strings for better refactoring support.
Using Tags¶
// Tag classes (usually empty marker classes)
public final class RedisTag {}
public final class InMemoryTag {}
// Tagged implementations
@Tag(RedisTag.class)
@Component
public final class RedisCache implements Cache {
// Redis implementation
}
@Tag(InMemoryTag.class)
@Component
public final class InMemoryCache implements Cache {
// In-memory implementation
}
// Selective injection
@Component
public final class UserService {
public UserService(@Tag(RedisTag.class) Cache cache) {
// Injects RedisCache specifically
}
}
@Component
public final class ProductService {
public ProductService(@Tag(InMemoryTag.class) Cache cache) {
// Injects InMemoryCache specifically
}
}
// Tag classes (usually empty marker classes)
class RedisTag
class InMemoryTag
// Tagged implementations
@Tag(RedisTag::class)
@Component
class RedisCache : Cache {
// Redis implementation
}
@Tag(InMemoryTag::class)
@Component
class InMemoryCache : Cache {
// In-memory implementation
}
// Selective injection
@Component
class UserService(@Tag(RedisTag::class) private val cache: Cache) {
// Injects RedisCache specifically
}
@Component
class ProductService(@Tag(InMemoryTag::class) private val cache: Cache) {
// Injects InMemoryCache specifically
}
Class Tags¶
Tags can be applied directly to component classes:
Method Tags¶
Tags can be applied to factory methods:
Annotation Tags¶
Create reusable tag annotations:
@Tag(RedisTag.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
public @interface RedisCache {}
@Tag(InMemoryTag.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
public @interface InMemoryCache {}
// Usage
@RedisCache
@Component
public final class RedisCacheImpl implements Cache {}
@Component
public final class UserService {
public UserService(@RedisCache Cache cache) {
// Injects RedisCacheImpl
}
}
@Tag(RedisTag::class)
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER)
annotation class RedisCache
@Tag(InMemoryTag::class)
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER)
annotation class InMemoryCache
// Usage
@RedisCache
@Component
class RedisCacheImpl : Cache
@Component
class UserService(@RedisCache private val cache: Cache) {
// Injects RedisCacheImpl
}
Special Tags¶
Special tag forms are useful when the default tag matching rules are too narrow. They let a component intentionally widen a request without losing type safety. This is most common with All<T>, where
you may want every implementation of an extension point, or every implementation that belongs to a specific tag group.
Use special tags sparingly. They are powerful because they change the meaning of a dependency request. A normal tag says "only this group"; Tag.Any says "ignore grouping"; Tag.All-style collection
requests say "collect the whole group".
@Tag.Any¶
Matches all components regardless of their tags:
@Tag.Any is the broadest request. It is useful when the consumer is intentionally generic, for example a registry, diagnostics component, or dispatcher that should see both tagged and untagged
implementations. Without Tag.Any, a tagged dependency normally matches only the requested tag set.
Because it widens the graph edge, Tag.Any should be visible in the constructor signature and used only where this broad behavior is part of the design. If a service only needs Redis caches or only
email notifiers, request that specific tag instead.
Tagged All¶
Get all components with a specific tag: This pattern collects all implementations that share a tag. It is useful when a subsystem has several implementations but they all belong to one named group, such as Redis-backed caches, public API interceptors, internal health checks, or a specific tenant/provider group.
The tag keeps the collection focused. Components of the same Java or Kotlin type can exist elsewhere in the graph without being included. That makes All<T> practical in larger applications where the
same interface may be reused for several independent purposes.
Tag Matching Rules¶
Tag matching is exact by design. Kora treats tags as part of the dependency identity, alongside the type. This prevents accidental injection of the wrong implementation when several components share an interface but belong to different contexts.
When a dependency does not resolve, check both the type and the tag. A component with the right type but the wrong tag is not a match. Likewise, an untagged dependency will not automatically pick a tagged component unless the request explicitly asks for that behavior.
- Exact Match: Tags must match exactly by class reference
- Inheritance: Tag classes can be part of inheritance hierarchies
- Multiple Tags: Components can have multiple tags
- Tag Filtering: Dependencies can specify required tags
What's Next¶
Now that you understand the core concepts of Kora's dependency injection system, you're ready to put it all together. Continue with Building Kora DI Applications for a step-by-step tutorial that builds a complete notification system and demonstrates these concepts in a practical context.
The tutorial covers:
- Project setup and multi-module structure
- External library modules with defaults
- Component override and customization
- Tagged dependencies and collection injection
- Optional dependencies and graceful degradation
- Submodules and component organization
- Generic factories and type-safe creation
- Lazy loading with
ValueOf<T>for performance optimization
Best Practices:
Components Small and Focused:
Why this matters: Small components are easier to test, understand, and reuse. Each component should have a single responsibility.
Beginner Tip: If your component is doing too many things, break it apart. Ask yourself: "What is this component's one job?"
Good Example:
// ✅ Single responsibility components
@Component
public final class OrderValidator {
public ValidationResult validate(Order order) { /* validation logic */ }
}
@Component
public final class OrderProcessor {
private final PaymentService payment;
private final OrderRepository repository;
public OrderProcessor(PaymentService payment, OrderRepository repository) {
this.payment = payment;
this.repository = repository;
}
public void process(Order order) {
// Just coordinates payment and storage
payment.processPayment(order);
repository.save(order);
}
}
// ✅ Single responsibility components
@Component
class OrderValidator {
fun validate(order: Order): ValidationResult { /* validation logic */ }
}
@Component
class OrderProcessor(
private val payment: PaymentService,
private val repository: OrderRepository
) {
fun process(order: Order) {
// Just coordinates payment and storage
payment.processPayment(order)
repository.save(order)
}
}
Constructor Injection:
Why this matters: Constructor injection makes dependencies explicit and prevents partially constructed objects. It's the safest and most testable injection method.
Beginner Tip: Always put dependencies in the constructor. Never create dependencies inside methods (that's "service locator" anti-pattern).
Good Example:
@Component
public final class UserService {
private final UserRepository repository;
private final PasswordEncoder encoder;
// ✅ All dependencies declared in constructor
public UserService(UserRepository repository, PasswordEncoder encoder) {
this.repository = repository;
this.encoder = encoder;
}
public User createUser(String email, String password) {
String hashedPassword = encoder.encode(password);
User user = new User(email, hashedPassword);
return repository.save(user);
}
}
@Component
class UserService(
private val repository: UserRepository,
private val encoder: PasswordEncoder
) {
// ✅ All dependencies declared in constructor
fun createUser(email: String, password: String): User {
val hashedPassword = encoder.encode(password)
val user = User(email, hashedPassword)
return repository.save(user)
}
}
Handle Optional Dependencies Gracefully:
Why this matters: Not all features are always available. Optional dependencies allow your application to work with different configurations.
Beginner Tip: Use @Nullable when a dependency might not be present. Always check for null before using.
Good Example:
@Component
public final class NotificationService {
private final EmailService emailService;
private final SmsService smsService; // Might not be configured
public NotificationService(EmailService emailService, @Nullable SmsService smsService) {
this.emailService = emailService;
this.smsService = smsService;
}
public void sendNotification(String message) {
emailService.sendEmail(message); // Always available
// ✅ Graceful handling of optional dependency
if (smsService != null) {
smsService.sendSms(message);
}
}
}
@Component
class NotificationService(
private val emailService: EmailService,
@Nullable private val smsService: SmsService? // Might not be configured
) {
fun sendNotification(message: String) {
emailService.sendEmail(message) // Always available
// ✅ Graceful handling of optional dependency
smsService?.sendSms(message)
}
}
Tags for Multiple Implementations:
Why this matters: Sometimes you need multiple implementations of the same interface (like different notification channels). Tags help you distinguish between them.
Beginner Tip: Create empty marker classes for tags. Use descriptive names like EmailNotification.class, not generic names.
Good Example:
// Tag classes
public final class EmailTag {}
public final class SmsTag {}
// Tagged implementations
@Tag(EmailTag.class)
@Component
public final class EmailNotifier implements Notifier {
public void notify(String message) { /* email logic */ }
}
@Tag(SmsTag.class)
@Component
public final class SmsNotifier implements Notifier {
public void notify(String message) { /* SMS logic */ }
}
// Usage
@Component
public final class AlertService {
private final Notifier emailNotifier;
private final Notifier smsNotifier;
public AlertService(
@Tag(EmailTag.class) Notifier emailNotifier,
@Tag(SmsTag.class) Notifier smsNotifier
) {
this.emailNotifier = emailNotifier;
this.smsNotifier = smsNotifier;
}
}
// Tag classes
class EmailTag
class SmsTag
// Tagged implementations
@Tag(EmailTag::class)
@Component
class EmailNotifier : Notifier {
override fun notify(message: String) { /* email logic */ }
}
@Tag(SmsTag::class)
@Component
class SmsNotifier : Notifier {
override fun notify(message: String) { /* SMS logic */ }
}
// Usage
@Component
class AlertService(
@Tag(EmailTag::class) private val emailNotifier: Notifier,
@Tag(SmsTag::class) private val smsNotifier: Notifier
)
Organize Components with Modules:
Why this matters: Modules group related components together, making your application easier to understand and maintain.
Beginner Tip: Create modules for different layers (database, services, HTTP) or business domains (messaging, notifications, user management).
Good Example:
// Individual messenger modules for different channels
@Module
public interface SlackModule {
@Tag(SlackMessenger.class)
@DefaultComponent
default Supplier<String> slackMessengerHeaderSupplier() {
return () -> "ASCII_PROTOCOL_MESSENGER_SLACK";
}
}
@Module
public interface SignalModule {
@Tag(SignalMessenger.class)
@DefaultComponent
default Supplier<String> signalMessengerHeaderSupplier() {
return () -> "ASCII_PROTOCOL_MESSENGER_SIGNAL";
}
}
@Component
public final class SlackMessenger implements Messenger {
private final Supplier<String> headerSupplier;
public SlackMessenger(@Tag(SlackMessenger.class) Supplier<String> headerSupplier) {
this.headerSupplier = headerSupplier;
}
@Override
public void sendMessage(String message) {
String header = headerSupplier.get();
System.out.println(header + " ---> " + message);
}
}
@Component
public final class SignalMessenger implements Messenger {
private final Supplier<String> headerSupplier;
public SignalMessenger(@Tag(SignalMessenger.class) Supplier<String> headerSupplier) {
this.headerSupplier = headerSupplier;
}
@Override
public void sendMessage(String message) {
String header = headerSupplier.get();
System.out.println(header + " ---> " + message);
}
}
// Application combines messenger modules
@KoraApp
public interface Application extends
SlackModule, // Slack messaging
SignalModule { // Signal messaging
}
// Individual messenger modules for different channels
@Module
interface SlackModule {
@Tag(SlackMessenger::class)
@DefaultComponent
fun slackMessengerHeaderSupplier(): Supplier<String> = Supplier { "ASCII_PROTOCOL_MESSENGER_SLACK" }
}
@Module
interface SignalModule {
@Tag(SignalMessenger::class)
@DefaultComponent
fun signalMessengerHeaderSupplier(): Supplier<String> = Supplier { "ASCII_PROTOCOL_MESSENGER_SIGNAL" }
}
@Component
class SlackMessenger(
@Tag(SlackMessenger::class) private val headerSupplier: Supplier<String>
) : Messenger {
override fun sendMessage(message: String) {
val header = headerSupplier.get()
println("$header ---> $message")
}
}
@Component
class SignalMessenger(
@Tag(SignalMessenger::class) private val headerSupplier: Supplier<String>
) : Messenger {
override fun sendMessage(message: String) {
val header = headerSupplier.get()
println("$header ---> $message")
}
}
// Application combines messenger modules
@KoraApp
interface Application :
SlackModule, // Slack messaging
SignalModule // Signal messaging
// Individual messenger modules for different channels
@Module
interface SlackModule {
@Tag(SlackMessenger::class)
@DefaultComponent
fun slackMessengerHeaderSupplier(): Supplier<String> = Supplier { "ASCII_PROTOCOL_MESSENGER_SLACK" }
}
@Module
interface SignalModule {
@Tag(SignalMessenger::class)
@DefaultComponent
fun signalMessengerHeaderSupplier(): Supplier<String> = Supplier { "ASCII_PROTOCOL_MESSENGER_SIGNAL" }
}
@Component
class SlackMessenger(@Tag(SlackMessenger::class) private val headerSupplier: Supplier<String>) : Messenger {
override fun sendMessage(message: String) {
val header = headerSupplier.get()
println("$header ---> $message")
}
}
@Component
class SignalMessenger(@Tag(SignalMessenger::class) private val headerSupplier: Supplier<String>) : Messenger {
override fun sendMessage(message: String) {
val header = headerSupplier.get()
println("$header ---> $message")
}
}
// Application combines messenger modules
@KoraApp
interface Application :
SlackModule, // Slack messaging
SignalModule // Signal messaging
Avoid Common Anti-Patterns:
❌ Service Locator Pattern:
❌ Circular Dependencies:
❌ Large Components:
// Don't create "God objects"
@Component
public final class HugeService {
// ❌ Does everything: validation, database, email, logging, caching...
private final Validator validator;
private final Repository repo;
private final EmailService email;
private final Logger logger;
private final Cache cache;
// Hundreds of methods...
}
// Don't create "God objects"
@Component
class HugeService(
// ❌ Does everything: validation, database, email, logging, caching...
private val validator: Validator,
private val repo: Repository,
private val email: EmailService,
private val logger: Logger,
private val cache: Cache
) {
// Hundreds of methods...
}
Best Practices¶
- Prefer constructor injection and let Kora build the dependency graph at compile time.
- Keep components focused on one responsibility so graph errors stay easy to understand.
- Use modules for reusable factories and default components, not as a place to hide application logic.
- Use tags only when the same contract has multiple meaningful implementations.
- Avoid service locators, circular dependencies, and large components that mix unrelated responsibilities.
Summary¶
You learned the core ideas behind Kora dependency injection:
- components declare what they need through constructors or component methods
- Kora validates and generates the dependency graph at compile time
- modules group reusable factories and default components
- tags disambiguate multiple implementations of the same type
- dependency injection keeps application structure explicit and testable
Troubleshooting¶
Component is not found:
- Check that the class is annotated with
@Componentor is returned from a@Modulemethod. - Verify that the module is included in the
@KoraAppinterface.
Multiple components match the same dependency:
- Add a tag to the dependency and to the component that should satisfy it.
- Keep tag classes or marker annotations close to the contract they disambiguate.
Generated graph does not compile:
- Read the generated error from the first missing or ambiguous dependency.
- Compile again after fixing one graph issue; later errors often depend on the first one.
What's Next?¶
- Build a Complete DI Application to practice modules, components, factories, tags, lifecycle, and graph design without HTTP noise.
- Create Your First Kora Application if you read this introduction first and now want to run a minimal app.
- Configuration with HOCON or Configuration with YAML after getting started, because configuration depends on having a runnable Kora app.
Help¶
If you encounter issues:
- check the Container documentation
- compare with the basic examples in Kora Examples
- review Creating Your First Kora Application for a runnable graph