Why configuration-driven
High-volume systems rarely have one right path through the platform. Configuration-driven orchestration lets you ship new flows, A/B them, version them, and roll them back without redeploying.
Four patterns to combine
- Strategy pattern: define an interface, implement concrete strategies, select via configuration.
- Reflection: dynamically instantiate classes from configuration — flexible, but watch for performance and type-safety.
- Functional composition: represent steps as functions/lambdas, compose with map/filter/reduce.
- Dependency injection: let a DI container wire components based on configuration; great for test isolation.
Strategy pattern in Kotlin
interface DataAccessor {
fun fetchData(): Data
}
class MongoDataAccessor : DataAccessor {
override fun fetchData(): Data { /* MongoDB logic */ }
}
class GrpcDataAccessor : DataAccessor {
override fun fetchData(): Data { /* gRPC call */ }
}
class DataOrchestrator(private val config: Config) {
fun fetchData(): Data {
val accessor = when (config.dataSource) {
"mongo" -> MongoDataAccessor()
"grpc" -> GrpcDataAccessor()
else -> throw IllegalArgumentException("Invalid data source")
}
return accessor.fetchData()
}
}Versioning is non-negotiable
Append version numbers to strategies (e.g. MongoDataAccessorV2), include version in configuration, and use a factory to select strategy + version. This makes evolution safe and rollbacks trivial.
Restructured the original Kotlin-heavy notes into a clearer four-pattern reference with tradeoffs and versioning guidance.
Originally published at /system-design/configuration-based-orchestration