For years, CI/CD configuration lived outside the codebase. You’d make changes in a UI that no one could review, no one could roll back, and no one could reproduce on a fresh install. The build pipeline was a shared mutable object and everyone was afraid to touch it.
TeamCity’s Kotlin DSL changed that for us. Here’s how we approached it.
The Setup: Monorepo with Vertical Slices
Our codebase is organised as a monorepo with vertical slices — each slice owns its domain end-to-end, from database migrations to API to frontend. The build configuration follows the same structure. Each slice has its own .teamcity folder with Kotlin DSL that defines its build chain. A shared library of reusable build steps lives at the root.
monorepo/
.teamcity/ # root project settings
slice-a/
.teamcity/ # slice-specific build chain
src/
slice-b/
.teamcity/
src/
The result is build configuration that reads like code, because it is code.
What Changes When CI/CD is in Git
Everything, actually. Build changes go through pull request review. You can see who changed a trigger and why. Rolling back a broken pipeline is a git revert. Onboarding a new service means copying an existing slice’s .teamcity folder and adapting it — no clicking through wizard screens.
Kotlin DSL also enables things that are awkward in the UI: conditional build steps, shared abstractions, typed configuration that catches errors at compile time rather than at runtime when your build fails at 2am.
Octopus Deploy Alongside It
Our deployment definitions live in Git too, using Octopus Deploy’s OCL Config as Code. The build produces an artifact, TeamCity hands it to Octopus, and Octopus handles environment promotion with the deployment process versioned right next to the code it deploys.
The principle is simple: if a change to how software is built or deployed isn’t in version control, it doesn’t exist in any meaningful way. Git is the source of truth. Everything else is derived from it.
