AI Coding Workflow: TDD, Reviews, and Guardrails

If you think AI-driven development makes you faster – probably you’re using it in wrong way. Faster development with AI produces more problems than benefits. Quality needs time and your attention.

I would like to focus you on problems related to AI-driven development, and how I reduce them:

  • AI doesn’t know the full context of the codebase and reinvents the wheel every single time.
  • AI doesn’t know project specifics, business domain and automations around it.
  • AI doesn’t know about all the constraints accepted in the project.
  • AI writes just to write, it has no goal to produce correct and proper code with respect to common practices.
  • AI will write a test to make it pass, not to actually test something.

All of these are big problems, using AI blindly just to get the work done doesn’t lead to anything good, the codebase degrades faster, amount of issues stacks up iteration by iteration and it becomes noticeable with growing your codebase.

To reduce the impact of such problems – you have to invest resources:

  • Into yourself because professionalism and expertise matters even more.
  • Into the way you are working with AI.

For a couple of months of active work with Claude and other agents I set up a working workflow for myself to reduce number of problems:

  • Firstly – spec work and plan of work, and only then coding. Such approach gives more predictable result for working with AI agents because each stage gives artifacts for the next one: specification, action plan, development.
  • Development is done using test driven development – firstly red tests, then the code to make tests pass, not the other way around otherwise at some point AI starts to generate tests only to pass the test coverage check.
  • Using IntelliJ IDEA MCP – it’s easy to connect through settings, MCP gives fast access to the code and line-by-line editing, and also use of different things of the IDE like file problem analysis.
  • Mandatory clean code review using multi-agent approach to find gaps in work. It helps to reduce amount of problems in code before the review stage. I documented seven AI code review limitations I found in production after running this workflow on real pull requests.

To build the same process you have to install a few plugins through claude -> plugins:

  • superpowers – framework for working in spec-driven way, there’s a link to the repository with detailed skill descriptions.
  • github – to get information about comments from GitHub and reviewing pull requests.
  • pr-review-toolkit – for multi-agent review.

My AI-driven development workflow

Here is an example how I’m uniting all of this staff into my development workflow

  1. Ask Claude: Let’s write a plan for feature: deep explanation of feature using brainstorming and writing-plans skills. Plan for implementation should be based on test-driven-development.
  2. Then comes the work with clarifications on the task and identifying different approaches, as a result you get a work plan you can execute using executing-plans. Such skill performs work step by step, validates at each stage and requests code review from itself.
  3. When the work is done – I check the code myself, dive in and edit when needed. Need for editing appears often even after long preparation.
  4. Ask Claude: Perform code review using pr review toolkit, then several different agents run with their own responsibility and at the output their results are combined to have less noise.
  5. As a result of code review, if there are any corner cases or obvious problems – I ask to write tests to reproduce them to understand the problem and fix it based on TDD methodology.
  6. The final step (for AI work) and intermediate step can be running the /simplify skill (part of review toolkit) so a separate agent checks what can be reused and reuses it. It helps to reduce code complexity and leads to proper reusing of functions instead of reinventing the wheel.
  7. Run deterministic guardrails for projects where they exist (highly recommend to setup), do manual testing of functionality, walk through the code and push if everything is ok.

This process takes more time (and tokens) than just asking AI to roll out some changes, but gives a predictable result matching expectations and most importantly – reproducible and maintainable one.

Keep in mind about AI-driven development

Context size

Context size – don’t use models with 1M context, firstly it’s very expensive after 100k tokens, and just not needed for our tasks.

Keep the context small – call /compact or create a new session /new, also for noisy tasks use subagent – they have their own context and the execution result can be received in the current chat without noise. Transformers underlying LLMs make emphasis on the beginning and the end of context while the middle suffers consistently.

This context problem also appears during review: large pull requests weaken AI code review in my production workflow.

Important: Don’t add plans and specifications to git, keep them for yourself, otherwise AI context will be filled with things irrelevant to the current task. I prefer to store them from time to time and clean once a week.

AI will try to trick you

AI will try to bypass tests and guardrails – you see @Suppress in the code – means AI couldn’t solve something and just decided to turn off the check. Sometimes it’s actually needed but it’s rare. If there’s a suppress – don’t be lazy, solve the problem, and only when it doesn’t work out – leave the suppress with your comment.

Remember: what matters is not the speed but the quality of the solution unless explicitly stated otherwise, every problem in the codebase will be multiplied too. AI-driven development is not an accelerator to reach goals, but a multiplier of the result, if the results are consistently bad – results will be worse over time.

Kotlin Guard Rails for AI-Driven Development

Step 7 above says “run deterministic guardrails” – let me show you what that actually means in practice. You have to care about your codebase. Here is my recipe for maintaining the Kotlin codebase to reduce bugs at scale. 

These days, generated code appears in the codebase, but recent research shows AI-driven development can lead to bloating your codebase with duplicates and bugs even faster than before. Even though smell code has always been a problem, it’s now widespread.

Each codebase should have guardrails, and it should always provide faster feedback than a human agent or a real dev, and better. It could be started by pre-commit checks or CI runs, or even together. Here is my setup for reducing smelly code. 

Detekt

Detekt is a static checker for Kotlin projects. It has a rich rule set that covers various aspects of the Kotlin language. It covers many different problems, but needs a small setup to choose not the default rules or adjust them so they are not too noisy. 

Kover

Kover for verification coverage. Maintainable code starts from unit tests, if you have them – you could sleep better knowing if something will be changed, unit tests highlighted. Yes, it depends on how you write tests, and even tests need validation, but it’s better to have them than not. Kover helps you get the test coverage percentage and fails if it exceeds the minimum threshold. 

PMD CPD

PMD CPD – relatively new integration that I started using, which needs to find duplicated code. It’s a very good tool because it helps you find duplicated code. When I configured it, I found more than 20 duplicates in a relatively new project, which was due to the AI-agent code generation. Even after code review, duplicates appear in the codebase. 

Konsist

Konsist – one more tool that I started using a few weeks ago. Konsist provides a way to write architecture tests, and this is so awesome. Developers or AI agents often miss some different parts of the architecture. Previously, it was caught during review, but why focus on it if you can automate? For example, with it, you can write dozens of tests for your project structure, making sure nobody can violate it. Are you tired of Spring repositories in controllers? No problem, you write an arch test, and it runs as a common unit test. Last month, I built an arch rule set for Spring Boot Best Practices and will soon share it publicly.

Konsist is required some time to configure, I did set of rules for Spring Boot projects covered different aspects of using Spring Boot powered by its Best Practices. You could read how to install this rule set from GitHub – Spring Boot Code Guard.

As example, using Spring Data JPA with Kotlin have some nuances, like you shouldn’t use data class together with entity classes, that rule will look like this, this is example from Spring Boot Code Guard:

    /**
     * Rule: @Entity classes should not be data classes.
     * Data classes are incompatible with JPA entities due to:
     * - Final by default (prevents lazy loading proxies)
     * - Structural equality in equals/hashCode (breaks entity identity)
     * - Immutability assumptions (conflicts with JPA state management)
     */
    val noDataClassEntityRule =
        object : SpringBootRule {
            override val description = "@Entity classes should not be data classes"
            override val suppressKey = "CodeGuard:noDataClassEntity"
            override fun verify(scope: KoScope) {
                scope
                    .notSuppressedClasses(suppressKey)
                    .withAnnotationNamed(SpringAnnotations.entityAnnotations)
                    .filter { it.hasModifier(KoModifier.DATA) }
                    .also { violations ->
                        if (violations.isNotEmpty()) {
                            val violatingClasses = violations.joinToString(", ") { it.name }
                            throw AssertionError(
                                "Data classes should not be used as @Entity: $violatingClasses. " +
                                    "Data classes are incompatible with JPA due to their final nature, " +
                                    "structural equality, and immutability assumptions.",
                            )
                        }
                    }
            }
        }

Kotlin compiler

Kotlin compiler – it highlights different problems that appeared in your project. By default, they don’t fail the whole process, so you have to activate it explicitly.

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
tasks.withType<KotlinCompile>().configureEach {
    compilerOptions {
        allWarningsAsErrors.set(true)
    }
}

How to use them together

My baseline is to have all four tools as one Gradle task that will: 

  • Run Detekt 
  • Run all tests and check coverage 
  • Run the duplication detector to highlight problems 
  • Run architecture tests Spring Boot projects

Finally it could be looks like this, in this example I intentiona

tasks.register("allChecks") {
    dependsOn("clean", "test", "cpdCheck", "detekt", "koverVerify")
    description = "Runs tests, Detekt, and Kover verification"
    group = "verification"
}

This task runs after the feature is finished. It runs automatically in CI and during AI agents development cycle as requirement. It helps a lot because of the short feedback on code changes – exactly what AI-driven development needs to stay maintainable at scale.

At the end

More than year ago I posted on topic – AI Will Take My Job, But Not Today, but nowadays I changed my mind, it will drastically change my job, and changed already.

Found this useful? Follow me on social media to stay updated.

Avatar photo
Dmitry Protsenko

Senior Software Engineer
Specialized on Java / Kotlin and CyberSecurity
Author of this blog

Articles: 34