Software engineering in era of AI
“It is our job to create computing technology such that nobody has to program. And that the programming language is human.”,
Jensen Huang
Software development has always been a dynamic field. The advent of Large Language Models (LLMs), such as OpenAI's GPT series, has introduced a new paradigm of AI-assisted software engineering, significantly enhancing the speed of code creation.
Nothing new comes without challenges, and developers, along with organizations, must be mindful of the potential complexities of code generation. Let's consider differences in concerns of ‘Rapid prototyping’ and ‘Traditional development’ and how we can overcome them.
Rapid prototyping
Key concern:
Speed – Prioritizing fast development cycles to quickly validate ideas, iterate on solutions, and adapt to changing requirements.
We already have AI-assisted tools that revolutionize rapid prototyping of ideas. Tools like bolt.new, v0.dev, screenshottocode.com can generate code for entire full-stack web and mobile applications.
The results are impressive and good enough to get customer feedback and quickly iterate, but they are not production-ready. Given the emphasis on speed in rapid prototyping, it is a great use-case for AI-assisted engineering.
Traditional development
Key concern:
Quality – Ensuring the generated code adheres to non-functional requirements (architectural characteristics) like reliability, maintenance, security, etc.
Let us find a balance between the speed that AI can bring and demands of a production environment.
Feature Level Quality - TDD with a twist
To address quality concerns, I propose to build on top of the concept of Test-Driven Development (TDD), which was introduced by Kent Beck in the late 1990s while working on Extreme Programming (XP).
The idea of TDD is simple: you write tests for your functions before writing the actual code to ensure that the code behaves as expected. If you need a quick refresher, I recommend Martin Fowler’s article on TDD.
3 key reasons it is better that way:
higher solution quality - well-structured, modular, and maintainable code
higher test quality - tests are not biased towards generated code
higher generated code quality - LLM will use tests to ensure that generated code meets requirements (self-verification & correction)
The TDD twist is that you need not only tests for generating code. You will need 3 things:
function signature with typified input and output parameters
detailed definition of function behavior
test-scenarios that cover all the aspects of expected function behavior
Code example in Python:
Code generation flow chart:
System Level Quality - Fitness Functions
You still need to cover non-functional requirements (NFRs), including security. To address this, I recommend leveraging the concept of the "architectural fitness function”, an idea introduced by ThoughtWorks architects and explored in the book Building Evolutionary Architectures.
“During test-driven development, we write tests to verify that features conform to desired business outcomes; with fitness function-driven development we can also write tests that measure a system’s alignment to architectural goals.”, ThoughtWorks’ FFDD (Fitness Function Drivem Developmemt)
What makes fitness functions effective? They’re automated, quantifiable, and continuous—integrated into your CI/CD pipeline. By embedding these system-level tests into your development process, you ensure that NFRs are consistently validated. For example, you might define a fitness function to measure response times under load (performance), enforce encryption standards (security), or verify system uptime (reliability).
Fitness functions act as guardrails, detecting deviations before they escalate into problems.
Summary
Rapid prototyping
There are already available tools for rapid prototyping that can generate code for entire full-stack web and mobile applications to quickly validate ideas, iterate on solutions, and adapt to changing requirements. Given the emphasis on speed during rapid prototyping, no additional considerations are typically required.
Traditional development
As LLMs can generate invalid, insecure, slow, etc. code, you need to maximize probability of valid code generation and protect other architectural characteristics (non-functional requirements) of a solution.
To ensure code quality, you should define before code generation:
function signature with typified input and output parameters
detailed definition of function behavior (what function should do)
test-scenarios that cover all the aspects of expected function behavior
To ensure solution quality, you should create a set of automated, quantifiable, and continuous—integrated into your CI/CD pipeline tests.
By embedding these system-level tests into your development process, you ensure that architectural characteristics are consistently validated. For example, you might define a test to measure response times under load (performance), enforce encryption standards (security), or verify system uptime (reliability).