Spec-Driven Development in practice
There are no new concepts in Spec-Driven Development. The idea of treating formal specification documents as the authoritative source of truth in software development has been around since the beginning of modern computing.
In the 1960s and 1970s, computer scientists developed formal methods to write mathematically verifiable specifications before implementing software. In the 1980s, Design by Contract introduced new metaphors to describe how the different components collaborate in a software system.
Behavior-Driven Development in the early 2000s established executable specifications (Given-When-Then) as living documentation that drives code development. And in the 2010s, API-First / Schema-Driven Design tools pioneered static code generation from contract definitions.
So what is Spec-Driven Development really, and why is everyone talking about it now?
In January 2026 Deepak Babu Piskala published “Spec-Driven Development: From Code to Contract in the Age of AI Coding Assistants”, where he defines this approach as inverting “the traditional workflow by treating specifications as the source of truth and code as a generated or verified secondary artifact.”
Piskala argues that because AI has made code generation fast and inexpensive, the primary bottleneck in software engineering has shifted from writing the code to clearly defining intent. He categorizes SDD adoption across a three-tier rigor spectrum:
- Spec-First: the specification guides initial code generation, but once the code exists the specification may or may not be maintained.
- Spec-Anchored: the specification is maintained alongside the code and changing the system’s behavior requires updating both the code and the spec.
- Spec-As-Source: the specification is the only artifact that we edit directly. Code is entirely generated from the spec and should never be updated manually. Any change to the system’s behavior means changing the spec and regenerating the code.
So the real shift in AI-native SDD is that we don’t write the specification before writing the code. We write it instead of writing the code. SDD is an engineering strategy where the developer’s role shifts from writing the implementation to architecting and maintaining the system specification.
For a more in-depth understanding of this approach, I recommend reading Piskala’s paper, or “Understanding Spec-Driven-Development: Kiro, spec-kit, and Tessl” by Birgitta Böckeler.
AI-Native SDD frameworks
There are several frameworks that use structured Markdown or YAML specifications to keep AI coding agents aligned, preventing drift across large codebases.
-
GitHub Spec Kit: An open-source toolkit designed to bring structured specification workflows to AI agents (like GitHub Copilot, Claude Code, and Gemini CLI) by enforcing a strict Specify → Plan → Tasks → Implement lifecycle.
-
Kiro (by AWS): An agentic IDE built specifically around spec-driven workflows that translates high-level prompts into structured requirements, system design documents, and executable task lists before writing code.
-
BMAD-METHOD: An agile-inspired, spec-driven framework that breaks AI development down into specialized agent roles (Product Manager, Architect, Scrum Master, Developer) governed by rigid PRD and architecture artifacts.
-
OpenSpec: A lightweight open-source CLI framework that captures requirements in structured Markdown and tracks change deltas (
ADDED,MODIFIED,REMOVED) alongside code. -
Traycer: An agentic workflow orchestrator that turns requirements into technical specifications, breaks them down into granular tasks, and coordinates parallel AI coding agents.
Main Challenges
Using LLMs to generate source code is not a trivial task. Large Language Models are Neural Networks, thus they are non-deterministic by nature (if you send the same request multiple times, you will get different results). They can also hallucinate, generating results that look correct but aren’t. As the project grows in complexity, maintaining a healthy codebase can become a challenge.
A machine learning model is just a pattern detector, and generative models reproduce the patterns learned during training.
Large Language Models are trained on tons of source code, every open source project that is publicly available plus thousands of software engineering books available online. This affects how useful these models can be: the more standard your project is, the more trustworthy the AI-generated solution will be.
We can make an analogy with AI for weather prediction here. While AI models like Google DeepMind’s GraphCast and NVIDIA’s FourCastNet have revolutionized meteorology by delivering predictions faster and with less computing power than traditional numerical methods, they are still limited by the fact that they are trained on historical data. They heavily struggle with out-of-distribution generalization.
In addition, every Machine Learning model has an error rate you cannot escape. Yes, you can simply ask the agent to generate “production quality code”, or to “apply industry standards”, but you still need to be familiar with those standards yourself, know the tooling and understand the trade-offs, and you will always need to validate the model output, either manually, by reading the generated code, or automatically by putting guardrails in place that ensure quality, or both.
Fortunately, in the Java ecosystem we have plenty of tools we can use to help with that validation. These are the same tools we use to maintain software quality when big teams collaborate on a shared codebase. In this article I will discuss a complete SDD example built in Java that is production-ready and can evolve cleanly thanks to these tools and a couple of key techniques I’ll cover later.
A Complete Spec Kit example
In this article, I walk through a complete production-ready service built with Spec Kit. I will not cover every detail about it; you can refer to the documentation if needed, but there are some basic ideas needed to understand how this works.
The specify CLI
This command initializes your project and manages extensions. The syntax to start a new project is:
specify init <PROJECT_NAME> --integration <AGENT_NAME>
Where <AGENT_NAME> is one of the supported coding agents.
When you initialize a Spec Kit project, a set of skills are generated and stored in the .agents folder. They will be part of your project, versioned with the rest of your artifacts.
A .specify folder will also be added, with internal framework assets.
The Agentic SDD workflow
Spec Kit defines a development workflow with up to nine steps. Each step is implemented by one of the skills that the specify command adds to your project:
/speckit-constitution(once per project, more details below)./speckit-specify, used to define the specification for your feature, from your natural language description./speckit-clarify(optional), used to ask any remaining questions about the new feature./speckit-plan, generates the design documentation. You can use it to provide the desired tech stack./speckit-checklist(optional), generates the validation criteria for the specification./speckit-tasks, determines the individual implementation steps, and resolves dependencies between them./speckit-analyze(optional) reports conflicts or gaps in the spec artifacts./speckit-implementexecutes the tasks in the required order. You can also scope it to a particular phase./speckit-convergeverifies the implementation against the spec, plan and tasks.
The Constitution
Spec Kit defines the concept of Constitution, a document describing the project’s governing principles that is read in every step.
The constitution is a Markdown file that is loaded on every downstream command. This content is critical, as the model will treat it as the foundational set of rules it must adhere to while generating specifications or code. This must be the first step in any project.
How the example project was built
I have published a complete REST service generated entirely with Spec Kit. It’s built with Micronaut, and the source code is available in this repository.
Using Piskala’s classification, this would be a spec-as-source project; everything, except for my prompts, has been generated by an LLM. Each branch shows the complete project at a specific stage so you can see clearly what was modified at each step.
I used different models depending on availability, though I won’t go into model-specific details here. It is true that every provider has its nuances, but the project does not depend on any one in particular.
I have kept my prompts for reference, and for most changes I asked the LLM to write a journal entry with details about the process, by adding this to the implementation prompt:
When you’re done, write a journal entry in docs/journal with your process to implement this feature: did you find any bugs? did you have to fix any issue found in static code analysis? did you need to add tests to meet minimum coverage?
1. Project Constitution
I liked this post by Daita, which proposes a series of templates that can be used depending on the type of project. I wanted to start from this “Backend / Systems Constitution” template, but make some changes to make sure it would prevent the LLM from generating unnecessary or non-requested features. So I used three different LLMs and asked them to improve Daita’s template. Then I asked one of them to evaluate the three prompts and select the best one for my goal. You can find the three alternatives and the assessment in the docs/prompts folder.
The selected option generated this constitution for the project. It contains four main sections:
-
Core Principles
- Minimum Viable Diff (Strict Scope Discipline)
- Ambiguity Halts Execution (Ask, Never Assume)
- Do Not Distribute by Default
- Optimize for Deletion, Not Extension
- Make Dependencies Explicit
- Contract at the Boundary, Not in the Middle
- Test the Transformation, Not the Plumbing
- Emit Structured Events, Derive Everything Else
- Recovery Over Prevention
- Attention Is Finite
- Value Is Realized at the User, Not at Merge
- Commands Are Discoverable; Local Dev Matches CI
-
Over-Engineering Defense & Operational Guardrails
- Crucial Meta-Rule: Anti-Gold-Plating Mandate
- Infrastructure Escalation Protocol
- Data Invariant & Semantic Integrity
- Decision-Ready Diff Audit Gate
-
Verification, Quality Gates & Review Standards
- Decision-Ready Diff Audit Gate
-
Governance
- Precedence Hierarchy
- Amendment Procedure
- Versioning Policy
- Compliance Review Expectations
The project constitution should not be linked to any specific technology. Nothing in this document mentions programming languages, frameworks, or libraries. As you can see, the project at this stage is technology agnostic.
2. Project Setup
The project setup prompt instructs the model to configure a project with a specific stack: Java 25, Gradle, Micronaut, MariaDB, RabbitMQ. It also defines the testing strategy, and the static analysis tools:
- ArchUnit will enforce strict architectural rules.
- SpotBugs will detect possible security vulnerabilities, performance issues or dodgy code.
- PMD will detect common programming flaws like high cyclomatic complexity, and unused code.
- Checkstyle will enforce code layout and naming conventions.
- Pitest will apply mutation testing to ensure test quality and coverage.
The prompt asks the model to generate all the required configuration files, including AGENTS.md, which enforces a few critical rules:
- Run the build command after every change, to ensure that all the checks pass.
- In case of error, analyze the error and fix the code. Never modify project build configuration without explicit authorization.
- Apply Aspect-Oriented Programming principles for crosscutting concerns (logging, security, transactions, etc).
- Always use the ASD-STE100 standard when writing documentation, commit messages and source comments.
Finally, the prompt also asks the model to document both the project setup and the static analysis policies.
At this point we have a safe project configuration we can use to generate new functionality. The constitution establishes clear principles the LLM must adhere to, reducing the risk of drift. The project configuration defines clear guardrails that block any change that reduces the code quality.
3. Product and Order Management
The project implements an e-commerce REST API that will allow users to manage two types of resources: Product and Order. I used two separate prompts to implement each feature.
The product management prompt defines the entity fields, and the REST endpoints to implement. You can inspect the spec.md, plan.md, and tasks.md that Spec Kit generates here, along with other supporting files.
The order management prompt does the same for orders and generates a new specification.
In both cases I asked the LLM to generate a journal entry and describe the process it followed, the initial generated code and the fixes it had to introduce to make the build pass:
This is a fragment found in the product management journal entry:
SpotBugs detected potential vulnerabilities in constructors that throw exceptions. The agent removed validation checks from constructors where Micronaut manages injection. SpotBugs also reported mutable state exposure in ErrorResponse. The agent converted the details list to an unmodifiable collection.
The order management journal entry also contains mutation testing errors:
Initially, the mutation testing score was 88 percent. Surviving mutations existed in CreateOrderRequest, UpdateOrderRequest, and OrderService.
The agent added focused unit tests to kill the surviving mutants.
You can also use the same process to refactor code. For example, I used this prompt to ask the LLM to change how the error management was implemented in the controller, by introducing ExceptionHandler classes. The generated specification contains the research and plan for making this change.
4. Asynchronous Notifications
The next iteration on this project was to introduce asynchronous notifications. Whenever an order is created or updated, the customer must be notified, and the system should support different transports. This prompt defines this feature, and this is the generated spec.
In this case, the research.md document is especially interesting. It documents decisions like the use of the @RabbitClient annotation, the creation of a NotificationTransportPort interface to allow for multiple transports, or the introduction of the OrderLifecycleEvent record to represent the contract at the messaging boundary.
The implementation journal entry documents a few interesting findings:
ArchUnit Domain Framework-Free Violation
The initial design declared @SerdeImport annotations on the messaging client targeting domain records. The Micronaut annotation processor generated serde classes inside the domain package. The generated classes depended on Micronaut framework packages. This violated the ArchUnit rule that requires a framework-free domain model. The agent resolved this issue by creating dedicated @Serdeable DTO records in adapter.messaging. The adapters convert between wire DTOs and pure domain models at the architectural boundary.
You can see the DTO and its static factory method here.
5. Local run and Load Testing
At this stage, it was time to see the service running. My next prompt asks the LLM to create the specification for a docker compose file that would orchestrate several containers to run the service and its dependencies: database (MariaDB), message queue (RabbitMQ), fake SMTP server (Mailpit), telemetry (Prometheus) and Grafana.
The prompt also asks for full documentation, with instructions to start and stop the containers, as well as the links to the corresponding control panels.
$ docker compose up -d
[+] Running 7/7
✔ Container sdd-mariadb Started 5.7s
✔ Container sdd-rabbitmq Started 5.7s
✔ Container sdd-mailpit Started 2.2s
✔ Container sdd-otel-collector Started 0.2s
✔ Container sdd-prometheus Started 0.4s
✔ Container sdd-app Started 5.9s
✔ Container sdd-grafana Started 0.5s

The prompt requested a collection of HTTP files that can be used to test the service functionality.
The next step was to add load testing tooling. I purposely sent a very short prompt to force the model to do some research and choose the best tools:
Add required tooling for load testing and performance validation.
Define scenarios for a configurable number of concurrent users hitting the products and orders endpoints with realistic payloads.
Prepare scripts that can be ran against existing docker compose setup to validate performance and stability.
The journal entry describes how the agent evaluated Grafana k6, Gatling, Locust, and JMeter. Finally, it chose Grafana k6 “because of low resource overhead, Docker execution, and built-in exit codes on threshold violations”.
The agent produced a load testing script, and the corresponding documentation.

$ ./load-tests/run.sh
==> Checking target service health at http://localhost:8080/health...
==> Target service is healthy.
==========================================================
Starting Load Test: mixed
Target: http://localhost:8080
VUs: 10
Duration: 30s
Ramp-up: 5s
==========================================================
==> Running k6 container (grafana/k6:latest)...
/\ Grafana /‾‾/
/\ / \ |\ __ / /
/ \/ \ | |/ / / ‾‾\
/ \ | ( | (‾) |
/ __________ \ |_|\_\ \_____/
execution: local
script: load-tests/scenarios/mixed.js
output: -
scenarios: (100.00%) 1 scenario, 10 max VUs, 1m0s max duration (incl. graceful stop):
* default: 10 looping VUs for 30s (gracefulStop: 30s)
running (0m01.0s), 10/10 VUs, 15 complete and 0 interrupted iterations
default [ 2% ] 10 VUs 00.5s/30s
...
running (0m30.0s), 10/10 VUs, 1242 complete and 0 interrupted iterations
default [ 98% ] 10 VUs 29.5s/30s
█ THRESHOLDS
http_req_duration
✓ 'p(95)<500' p(95)=153.19ms
✓ 'p(99)<1000' p(99)=176.3ms
http_req_failed
✓ 'rate<0.01' rate=0.00%
█ TOTAL RESULTS
checks_total.......: 1414 46.078354/s
checks_succeeded...: 100.00% 1414 out of 1414
checks_failed......: 0.00% 0 out of 1414
✓ view product detail status is 200
✓ add product status is 201
✓ checkout order status is 201
✓ verify created order status is 200
✓ browse catalog status is 200
HTTP
http_req_duration..............: avg=73.77ms min=1.89ms med=111.54ms max=289.56ms p(90)=143.64ms p(95)=153.19ms
{ expected_response:true }...: avg=73.77ms min=1.89ms med=111.54ms max=289.56ms p(90)=143.64ms p(95)=153.19ms
http_req_failed................: 0.00% 0 out of 1416
http_reqs......................: 1416 46.143529/s
EXECUTION
iteration_duration.............: avg=236.68ms min=104.29ms med=246.43ms max=479.19ms p(90)=313.51ms p(95)=325.93ms
iterations.....................: 1272 41.450966/s
vus............................: 10 min=10 max=10
vus_max........................: 10 min=10 max=10
NETWORK
data_received..................: 4.2 GB 137 MB/s
data_sent......................: 884 kB 29 kB/s
running (0m30.7s), 00/10 VUs, 1272 complete and 0 interrupted iterations
default ✓ [ 100% ] 10 VUs 30s
==> Load test completed successfully. All thresholds passed.
6. Authentication & Authorization
I intentionally left out authentication and authorization at first. I wanted to add it to an existing service to test how well the agent would be able to add a cross-cutting concern to an existing codebase.
In my prompt I described how the authentication had to work (JWT tokens) and how the role-based authorization should enforce several rules:
- Initial roles are ADMIN, CUSTOMER, CATALOG
- Each user has one or more roles assigned. CUSTOMER is the default role for new users.
- Any user can access the login endpoint
- Any CUSTOMER user can list products, but only users with CATALOG role can see draft or archived products.
- Any CUSTOMER user can read an active product, only users with CATALOG role can see draft or archived products.
- Only users with CATALOG role can create or update products.
- Any CUSTOMER user can create orders
- Only ADMIN users can modify orders
- CUSTOMER user only see their own orders
- ADMIN users can list or read any order
The agent asked some clarifying questions, for example:
Is user registration open to unauthenticated users with only the CUSTOMER role, or can registration requests specify roles (or require ADMIN privileges)?
Option Answer Implications A Public registration always assigns only CUSTOMER;ADMINandCATALOGrequire anADMINSecure by default; prevents privilege escalation while allowing customer self-signup. Requires seeded or admin-managed roles B Public registration accepts optional rolesand defaults toCUSTOMERif omittedSimplifies initial testing/setup, but allows any caller to self-assign ADMINorCATALOGunless restrictedC Registration is restricted to ADMINusers onlyStrictest control, but customers cannot self-register without an administrator Custom Provide your own answer Specify who may register users and how ADMIN/CATALOGroles are assigned
Ultimately, it produced a valid implementation along with additional test requests to verify that it works. The generated research document describes the password hashing algorithm used, and the rationale behind that decision:
** 2. Password Hashing **
Decision Use JDK-standard
PBKDF2WithHmacSHA256(javax.crypto.SecretKeyFactory) with a 16-byte random salt (SecureRandom), 310,000 iterations, and constant-time comparison (MessageDigest.isEqual) behind aPasswordHasherPortoutbound port inapplication.port.out.Rationale
- Requires zero additional third-party dependencies.
- Follows OWASP Password Storage Cheat Sheet recommendations for PBKDF2-HMAC-SHA256.
- Keeps
domainframework-free (Userstores only the opaquepasswordHashstring, never plaintext).Alternatives Considered
- BCrypt (
at.favre.lib:bcryptorspring-security-crypto): Strong adaptive hash, but introduces an extra external dependency not currently ingradle/libs.versions.toml. Can be swapped behindPasswordHasherPortlater if desired.
You can explore the source code at this stage here.
7. Bug Fixing
You can also apply SDD to bug fixing. In Spec Kit, this requires installing an official extension that adds three new skills to the project:
/speckit-bug-assessfor bug triaging./speckit-bug-fixto apply the fix described in the assessment./speckit-bug-testto validate the fix by reproducing the issue and recording new evidence.
I identified a bug in the order creation logic: product ids were not validated, so it was possible to create orders with non-existent products. This was my prompt to start the investigation:
/speckit-bug-assess When an order is created, product ids are not validated, making possible to create an order with unexisting products.
This generated a complete assessment including steps to reproduce the issue, root cause analysis and proposed remediation:
Root Cause Hypothesis
Confidence: high. The
OrderServiceapplication service does not depend onProductRepositoryPort. The service does not check whether product identifiers inCreateOrderCommandexist in the database. In addition, theorder_linesdatabase table has no foreign key constraint that references theproductstable. Because no layer validates product existence, the application creates orders with fictitious product identifiers.Proposed Remediation
Preferred: Inject
ProductRepositoryPortintoOrderService. InOrderService.createOrderandOrderService.updateOrder, iterate through the order lines. Verify that each product identifier exists viaproductRepository.existsById(productId). If a product does not exist, throw anInvalidOrderDataExceptionwith a descriptive message. TheOrderExceptionHandlerwill translate this exception into HTTP 400 Bad Request.Alternatives:
- Alternative 1: Throw
ProductNotFoundException. TheProductExceptionHandlerwill return HTTP 404 Not Found. Trade-off: HTTP 404 onPOST /v1/ordersimplies that the endpoint does not exist instead of signaling a payload validation failure.- Alternative 2: Add a foreign key constraint in a Liquibase changeset from
order_lines.product_idtoproducts.id. Trade-off: This enforces integrity at the database level, but unhandled database exceptions will cause HTTP 500 responses instead of user-friendly validation errors.
You can review the proposed solution, send a follow-up prompt if you’re not happy with the selected option, or otherwise use the fix skill which will implement the changes and generate a fix report.
8. Adding a more complex feature
The amount of work saved by using AI at this point is impressive. However, the generated code is almost completely boilerplate. There is no complex logic here.
I wanted to challenge the agent by introducing a more complex feature to see how it would perform on an already sizable codebase.
The new feature would be campaign management. In this iteration I asked the model to:
- Add a new
Categorydomain entity, make every product belong to at least one category, and make the corresponding endpoints ADMIN only. - Introduce the
Campaignaggregate, which would also include aPredicateentity for complex campaign logic. - Modify the
Productendpoints to include a list of applicable campaigns for each product.
The model asked a few clarifying questions:
Is categoryId optional or required on Product, and if required, how should existing uncategorized products and backward compatibility on existing product operations be handled?
Option Answer Implications A Optional ( nullable) on/v1/productsPreserves full backward compatibility for existing /v1/productscallers and stored products (categoryIdmay benull); validates whenever non-null.B Required on /v1/products(breaking change with migration backfill)Seeds a default category and backfills existing products via Liquibase; existing product create/update requests, tests, and load tests must supply categoryId.C Optional on /v1/products, required on/v2/productsPreserves /v1/productsbackward compatibility while enforcing mandatorycategoryIdon a new/v2/productsresource version.Custom Provide your own answer Reply with Q1: Custom - <details>specifying optionality and migration/versioning rules.
Note how the model here is proposing to use endpoint versioning, as we are introducing a breaking change.
Also, I didn’t specify how multiple campaign predicates would be combined, so the model asked about it:
How are multiple predicates on a campaign combined (AND vs. OR), how is an empty predicates list treated, and are price threshold comparisons strict (< / >) or inclusive (<= / >=)?
Option Answer Implications A Logical AND; empty list matches all products; strict</>comparisons in centsEnables price bands ( IS_MORE_EXPENSIVE_THAN+IS_CHEAPER_THAN) and combined rules; uncategorized products (categoryId == null) matchNOT_IN_CATEGORY.B Logical OR; empty list matches no products (or is rejected)Allows targeting multiple categories ( IN_CATEGORYA or B) in one campaign, but cannot combine category + price constraints withAND.C Logical AND; at least one predicate is required; strict</>comparisons in centsSame ANDsemantics as Option A, while rejecting campaigns created or updated with an emptypredicateslist.Custom Provide your own answer Reply with Q2: Custom - <details>specifying boolean combination, empty-list handling, and boundary comparison semantics.
The final question was regarding the specific modeling of the available discounts in the Product resource:
What active discount fields must the product response include (list of active discounts only vs. also a computed discounted price), and how are multiple simultaneous active campaigns resolved (list all, apply highest discount, or stack)?
Option Answer Implications A List of matching active discounts only (e.g., campaignId,campaignName,discountPercent)Product HTTP response exposes all matching active discounts (empty list when none) and leaves priceInCentsas the unmodified catalog price.B List of matching active discounts plus discountedPriceInCentsusing highest discount onlyExposes all matching active campaigns while computing a single effective price from the maximum discountPercentamong them.C List of matching active discounts plus discountedPriceInCentsby stacking discountsExposes all matching active campaigns and computes an effective price by stacking discounts (either additively capped at 100% or multiplicatively). Custom Provide your own answer Reply with Q3: Custom - <details>specifying the exact product discount response fields and multi-campaign resolution rule.
The generated code is well structured, follows the project standards, and uses modern Java features:
The Campaign entity has a matches(Product product) method that evaluates the predicates:
package es.nachobrito.sdd.example.domain.model;
public record Campaign(
UUID id,
String name,
@Nullable String description,
CampaignStatus status,
Instant startDate,
Instant endDate,
int discountPercent,
List<CampaignPredicate> predicates) {
//...
/**
* Evaluates whether a product matches all campaign predicates.
*
* @param product the target product
* @return true if predicates is empty or every predicate matches the product
*/
public boolean matches(Product product) {
Objects.requireNonNull(product, "Product cannot be null");
return predicates.stream().allMatch(predicate -> predicate.matches(product));
}
//...
}
The CampaignService contains logic to resolve the discounts for a given list of products, and the ProductResponse DTO formats the active discounts as part of the REST response.
You can explore the project at this stage here.
9. Documentation generation
Besides the spec generation, which is the source of truth for the service development, human-friendly documentation is also required for a production-ready software project.
LLMs excel at text generation in terms of quantity, but not always in quality. Remember we added to the AGENTS.md clear instructions to use the ASD-STE100 standard.
4. Write documentation in ASD-STE100
Always use ASD-STE100 Simplified Technical English when you write documentation, commit messages, and source comments:
- Use short sentences. Procedural sentences: 20 words maximum. Descriptive sentences: 25 words maximum.
- Write one instruction in each sentence.
- Use the active voice. Use the imperative form for instructions.
- Use only approved words, with one meaning for each word. Use the technical names from this project.
- Do not use phrasal verbs, contractions, or slang.
- Use articles (“the”, “a”) where possible.
This is not the only option; some of my native English colleagues prefer to use the Oxford Guide to Plain English as reference instead.
But in any case, some kind of steering is needed when generating human-readable documents, to avoid ending up with a wall of text. As a final request, I asked the model to generate a complete technical design document for this service, describing the architecture, the domain model and the main use cases.

The final result is a complete REST service that is well documented, backed by strict static analysis rules that guarantee code consistency, and comparable in quality to any service running in production, yet built in a fraction of the usual time.
What does this mean for Software Engineering?
I am reaching the same conclusion as many of my colleagues. As of November 2026, this technology is powerful enough to generate production quality software in a fraction of the time it would take to write it manually. Just think about the amount of code, documentation and scripts you have seen. Before LLMs this would have meant weeks of work. Yet it took me the equivalent of two days with the help of AI.
However, I still see the need for a human who knows what production quality software is like. I know I can safely trust 95% of this code because I know the tooling, and the concepts:
- I know that the logic is correct because the tests are green.
- I know the tests are correct because I know mutation testing ensures test completeness.
- I know the service architecture will remain consistent because ArchUnit will enforce it.
- I know the code will stay clean because SpotBugs and PMD will apply strict rules.
For each rule, I also know and understand the trade-offs, and the necessary exceptions configured for this project.
But, more critically, I know I must carefully review the remaining 5% of the source code. I need to verify that the password hashing is done properly, for example. As we saw in Section 6, the model followed an outdated OWASP recommendation of 310,000 iterations for PBKDF2WithHmacSHA256, instead of the current 600,000 minimum (or Argon2id). Static analysis will not catch that. If this were a real production service, I would own this code and any security issue would be my responsibility, not the AI model’s.
I’ve been building software for more than twenty-five years now. Like many other developers, I’ve gone through several phases with respect to AI-assisted software development: from skepticism to excitement, and from full confidence that AI will never replace humans to a realization that it really can handle many tasks only humans were capable of two years ago. To me one thing is clear: the way we build software has changed forever.
I think every developer falls somewhere on a spectrum between two types. “Coders” enjoy solving complex logical problems and take pride in the quality of their source code, how beautiful and elegant their solution is. “Builders,” on the other hand, just want to create useful things. They want to make computers work for people and solve real-world problems.
Those who lean toward the coder side feel a sense of loss now that AI can generate code for them. Those on the builder side, however, love being able to build more in the time they have. I lean more toward the builder side, and enjoy the fact that I can set up a whole REST endpoint with telemetry, async notifications, strict quality checks and complete technical docs in just one afternoon. Still, I understand the sense of loss felt by those who miss solving complex problems directly in code; I miss that sense of accomplishment too.
Still, I want to end on a positive note. The fact that we can now build software far faster, and with much less effort, does not mean that we are going to be replaced by AI. Instead, it means we will be able to tackle much harder problems that seemed out of reach until now. I am convinced that in a few years, there will be far more software engineering positions, not fewer.