A simple, unopinionated framework for booting applications. Intended to be much simpler than Spring Boot or even modern iterations like Quarkus and Micronaut, but still featureful enough to be useful. Handles stopping, starting, lifecycle management, logging, configuration, and other basic reusable components.
Bootable’s core is plain Kotlin and embeds no dependency-injection framework. Its runtime takes two sets — your application services and your lifecycle controllers — and runs them; how you assemble those sets is entirely your choice. Wire them by hand, use a DI solution provided by Bootable as optional add-on artifacts, or bring your own DI. Bootable ships optional bindings for Kodein-DI (clean DSL syntax but executes at runtime) and Metro (complex annotation-based syntax, but executes and checks for a valid dependency graph at compile-time). These are selectable per module, and you can just as easily integrate with any other DI, or none at all.
An important element of simplicity is to prefer the use of explicit references and configuration.
Spring Boot’s runtime discovery and configuration of components and plugins via annotation and reflection is easy but not simple.
To use Spring Boot, you have to understand two layers: the libraries underneath Spring, as well as all of Spring Boot’s own auto-magic annotations.
Many users of Spring Boot fail to understand the hidden complexities, because they never really understand what Spring Boot’s annotations are doing (ever seen @Transactional on a private method — it silently does nothing at all, potentially making that code non-atomic).
Bootable aims to be both easy and simple. The goal of Bootable is to provide a minimal framework for running Kotlin (or Java?) applications, and then gets out of your way.
Useful reading:
|
Note
|
Importing the Metro DI artifact does bring annotations to your application, BUT the annotations are limited to the DI layer — the application’s own behavior is not controlled by annotations. |
-
Runs a set of application services and lifecycle controllers — assemble them however you like (plain Kotlin, Kodein, Metro, a service loader, …). Bootable embeds no DI framework; optional bindings for Kodein and Metro are available per module. See the
examples/directory for one project per wiring style. -
Manages the lifecycle of application services.
-
Allows registration of lifecycle controllers, which can be used to control the lifecycle of application services in various ways. The basic built-in implementation of this is the
AppStartStopLifecycleControllerwhich starts application services on app start, and shuts them down on app close, in priority order. Another basic implementation, intended to be used in conjunction withAppStartStopLifecycleController, is theStopSignalHandlerLifecycleController, which registers signal handlers, which, when received, cleanly shuts the system down. Other implementations are possible: for example starting/stopping services at runtime based on a command system, like start/stop messages from RPCs or web requests.
Bootable is modular, and provides a number of modules to help you build your application.
The core module is simply com.github.rocketraman.bootable:boot, which provides the basic application lifecycle features of Bootable.
Add additional modules as you wish.
See hoplite.
Add this module to your project to use Hoplite for configuration.
|
Warning
|
Over-using this abstraction is a code smell. |
This module simply exposes a strongly-typed global environment variable to the system, which is initialized from configuration by a lifecycle controller before services start. With vanilla wiring, add environmentInitializer(binder) to the controllers you pass to boot; with Kodein or Metro, this module’s optional bindings provide it for you.
This should be rarely used, but sometimes it is useful to have a way to identify the environment e.g. dev, test, prod, or other. Good use cases are tagging metrics with the environment, or extremely complex environment-specific configuration that can’t be easily abstracted using one of the Configuration modules.
Note that this is also useful in a JavaScript environment, where the environment may be bound to a global variable by something like dotenv-webpack. The environment abstraction may be used to abstract away the underlying details of this binding in a way that can be used in both frontend and backend in the common code source set.
|
Note
|
We currently do not publish multi-platform versions of these modules, but may do so in the future. |
This module provides all the necessary imports to configure Log4j2 as the logging system for the application. It provides good default logging layouts, and supports multiple output types e.g. plain (both color and not), JSON, and GCloud. See LoggingType.
Provides a basic Ktor service abstraction with Bootable lifecycle integration i.e. the server starts and stops with the application. Does not attempt to configure Ktor in any significant way — this is left to the user. See KtorService.
See Wiring Bootable for the full guide, and the projects in the examples directory — there is one per wiring style, so you can copy the one that matches your choice of DI (or no DI).
The essentials, regardless of wiring:
-
Application components that have a lifecycle implement
AppServiceorAdvancedAppService. -
You assemble a
Set<AppService>and aSet<LifecycleController>, and hand them to thebootfunction, which runs them. With vanilla wiring that is justboot { Bootable(setOf(MyService(…))) }—lifecycleControllersdefaults todefaultLifecycleControllers(). -
defaultLifecycleControllers()provides the built-in start/stop and signal-handling controllers. To run a custom lifecycle — e.g. starting/stopping services on command from an RPC or web request rather than at boot — pass your own controllers instead.
To use a DI framework, depend on the -kodein or -metro artifact of each module you use (e.g. boot-kodein, boot-config-hoplite-kodein, boot-env-kodein, or the -metro equivalents) and let it assemble the sets:
-
Kodein (runtime): import the provided
bootableModule/hopliteConfigModule/environmentModule, contribute your services withbindAppService { … }, and pass the resolved sets toboot. -
Metro (compile-time): declare a
@DependencyGraphthat extendsBootableGraph, contribute your services with@ContributesIntoSet, andboot { createGraph<AppGraph>().bootable }.
The heavy-weight nature of DI frameworks like Spring has given DI a bit of a bad rap, and many people argue it is unnecessary in Kotlin — that simple constructor injection is enough.
This is absolutely true for small projects, but as a project grows the need for modularity arises: consider a multi-module app, each module of which exposes one or more AppService implementations without any central code being aware of them all.
Collecting those into a Set<AppService> is exactly what a DI container’s multibindings do well — but it is also easy to do by hand, and different teams reasonably prefer different tools (or none).
Bootable’s core is plain Kotlin: the AppService / LifecycleController interfaces, the built-in controllers, and the main framework class Bootable.kt carry no DI annotations, and Bootable takes its two sets through an ordinary constructor.
The seam is those two sets; how you fill them is up to you.
For convenience, each module that has anything to wire ships optional Kodein and Metro bindings as separate -kodein / -metro artifacts (e.g. boot-metro), each depending on the plain module, so you can opt into a framework per module — one ordinary dependency, no Gradle plugin or capability selection — without the core ever depending on one.
Kodein has, to my eye, a clean simple DSL syntax, BUT no compile-time graph checking, and is slightly slower to initialize on application startup since it wires the graph at runtime.
Metro has a more verbose and complex annotation-based syntax, but the payoff is compile-time verification and construction of the graph.
See these three wiring options:
-
❏ Multiple logging implementations for different scenarios, loggingInit via
ServiceLoader? -
❏ Create a boot-server-http-ktor-cohort module using Cohort
-
❏ Update the ktor example to integrate Kompendium — this is outside the scope of Bootable, but it would be a good example of how to integrate Bootable with other libraries
-
❏ Make multiplatform — some modules are backend only e.g. Hoplite, Log4j2, but others could easily be adapted for multiplatform usage
-
❏ Integrate more useful modules e.g. JetBrains Expose, postgresql, and so on
Raman Gupta <rocketraman@gmail.com>