A very common phrase nowadays is "the code made by AI is not as good as the one I write".
This phrase might have been valid a year ago or even a few months back, but since the launch of Claude 3.7 Sonnet, this has changed. This, combined with tools with Agents such as Cursor, Cline, or Jetbrains Junie.
In this post, we will see how with a prompt (somewhat complex, indeed) we can generate quality DDD code. To apply it, you don’t need to have any .cursor/rules
or .junie/guidelines.md
, although having them will make the generated code even better.
Everything is based on defining an aggregate, with its properties, domain events, invariants, corrective policies, and access methods.
To generate it, you simply need to fill in the following template and add it at the end of the prompt:
* Name:
* Description:
* Context:
* Properties:
* …
* Enforced Invariants:
* …
* Corrective Policies:
* …
* Domain Events: …
* Ways to access: …
We have called this template Codely's Aggregate Design Blueprint and it is what we will use to generate the code. It is a text version and an iterated one of the aggregate design canvas.
🔢 Prompt Structure
The prompt may seem very complex, but it is actually composed of 5 simple pieces to understand:
1️⃣ Who you are
Indicate to the AI that you are an expert programmer and an expert in DDD. This will let the AI know what its role is.
2️⃣ How the Codely Blueprint structure is
Here we explain what fields it has and what definition there is for each one. This part could be migrated to a JSON Schema, which would make it stricter. In this case, we preferred not to do it this way, as it is easier to edit when it is direct markdown.
3️⃣ How to transform the Codely Blueprint to code
Here we indicate step by step how it has to transform the Codely Blueprint to code.
4️⃣ Protocol to execute the transformation
This part is what makes the generated code go from being 60% correct to about 80% of how we would do it by hand. One of the interesting things we tell it is to do TDD, something that is not common in code generation. This makes it go case by case and the resulting code is of higher quality.
It is important to emphasize that at no point are we explicitly telling it to run the tests. Depending on the model and configurations you have set up, it may do so, but even if it doesn’t run them, it greatly improves the quality of the resulting code.
5️⃣ User Variables
Here is where you can modify the prompt to fit your code. In this case, we have defined two variables which are $FOLDERS_CASE
and $FILES_FORMAT
. The first is to indicate how the folders should be named and the second to indicate the format of the files.
Then we have the Codely Aggregate Design Blueprint, which is where the aggregate data is filled in.
✍️ Prompt to generate a DDD aggregate with AI
Copy and paste the following prompt into your editor and modify from the User variables section to fit your code.
It is important to run it in agent mode:
You are an expert programmer and a DDD expert. You'll be given a Codely's Aggregate Design Blueprint and have to transform it to code.
# Codely Aggregate Design Blueprint structure:
'''
* Name: The name of the aggregate.
* Description: A brief description of the aggregate.
* Context: The context where the aggregate belongs.
* Properties: A list of properties that the aggregate has. Optionally, you can specify the type of each property.
* Enforced Invariants: A list of invariants that the aggregate enforces.
* Corrective Policies: A list of policies that the aggregate uses to correct the state of the aggregate when an invariant is violated.
* Domain Events: A list of events that the aggregate emits.
* Ways to access: A list of ways to access the aggregate.
'''
# Instructions to transform the Aggregate Design Blueprint to code:
You have to create:
* A module for the aggregate:
* The module name should be the name of the aggregate in plural.
* Should be written in $FOLDERS_CASE.
* Should be inside the `src/contexts/$CONTEXT_NAME` directory.
* Every module contains 3 folders: `domain`, `application`, and `infrastructure`.
* Inside the `domain` folder, you'll have to create:
* An `$AGGREGATE_NAME.$FILES_FORMAT file that contains the aggregate class:
* The file name should be the name of the aggregate in PascalCase.
* The aggregate class should have the properties, invariants, policies, and events that the aggregate has.
* You should take a look to other aggregates to see the format.
* A `$DOMAIN_EVENT.$FILES_FORMAT file per every event that the aggregate emits:
* The file name should be the name of the event in PascalCase.
* The event should have only the mutated properties.
* You should take a look to other events to see the format.
* A `$DOMAIN_ERROR.$FILES_FORMAT file per every invariant that the aggregate enforces:
* The file name should be the name of the invariant in PascalCase.
* You should take a look to other errors to see the format.
* A `$REPOSITORY.$FILES_FORMAT file that contains the repository interface:
* The file name should be the name of the aggregate in PascalCase with the suffix `Repository`.
* The repository should have the methods to save and retrieve the aggregate.
* You should take a look to other repositories to see the format.
* Inside the `application` folder, you'll have to create:
* A folder using $FOLDERS_CASE for every mutation that the aggregate has (inferred by the domain events) and for every query that the aggregate has.
* Inside every query/mutation folder, you'll have to create an `$USE_CASE.$FILES_FORMAT file that contains the query/mutation use case.
* The file name should be the name of the query/mutation in PascalCase in a service mode. For example:
* For a `search` query for a `User` aggregate, the class should be `UserSearcher.$FILES_FORMAT.
* For a `create` mutation for a `User` aggregate, the class should be `UserCreator.$FILES_FORMAT.
* You should take a look to other queries/mutations to see the format.
* Inside the `infrastructure` folder, you'll have to create:
* A `$REPOSITORY.$FILES_FORMAT file that contains the repository implementation:
* The file name should be the name of the aggregate in PascalCase with the suffix `Repository`.
* Also, the file should have an implementation prefix. For example, for a `User` aggregate and a Postgres implementation, the file should be `PostgresUserRepository.$FILES_FORMAT.
* The repository should implement the repository interface from the domain layer.
* You should take a look to other repositories to see the format and use the most used implementation.
* You'll have to create a test per every use case:
* The test should be inside the `tests/contexts/$CONTEXT_NAME/$MODULE_NAME/application` directory.
* You should create an Object Mother per every aggregate and value object that you create inside `tests/contexts/$CONTEXT_NAME/$MODULE_NAME/domain`.
* Take a look inside the `tests/contexts` folder to see the format of the Object Mothers and the tests.
* You should only create a test per every use case, don't create any extra test case.
* You should create a test for the repository implementation:
* The test should be inside the `tests/contexts/$CONTEXT_NAME/$MODULE_NAME/infrastructure` directory.
# Protocol to execute the transformation:
## 1. Search for the examples of the files that you have to create in the project
Execute `tree` to see the current file structure. Then use `cat` to see the content of similar files.
## 2. Create the test folders structure
If the module folder doesn't fit inside any of the existing contexts, create a new one.
## 3. Create the test for the first use case
* We should create use case by use case, starting with the first one.
* We're doing TDD, so we'll create the first use case test first.
* Also, we'll create all the object mothers.
* Then all the domain objects (if needed).
* Then the use case.
* Do it until the created test passes.
* Repeat this per every use case.
## 4. Create the repository implementation test
* We should create the repository implementation test after all the use cases are created.
* First, create the repository implementation test.
* Then, create the repository implementation.
* Do it until the created test passes.
# User variables:
$FOLDERS_CASE = kebab-case
$FILES_FORMAT = ts
# User Codely Aggregate Design Blueprint:
'''
* Name: Naive Bank Account
* Description: An aggregate modeling in a very naive way a personal bank account. The account once it's opened will aggregate all transactions until it's closed (possibly years later).
* Context: Banking
* Properties:
* Id: UUID
* Balance
* Currency
* Status
* Transactions
* Enforced Invariants:
* Overdraft of max £500
* No credits or debits if account is frozen
* Corrective Policies:
* Bounce transaction to fraudulent account
* Domain Events: Opened, Closed, Frozen, Unfrozen, Credited
* Ways to access: search by id, search by balance
'''
✨ Future Improvements
It would be ideal to have the Blueprint of each aggregate in a file at the root of its module and that, when you want to add a new use case, you simply have to modify that blueprint to add a new event or access method.
This would allow the AI to infer what you need to do and simply by executing the prompt, the necessary code would be generated. And if that use case already exists, it would be omitted. Coming soon. 🙌