<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Ronnie Schaniel</title>
	<atom:link href="https://ronnieschaniel.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://ronnieschaniel.com/</link>
	<description>Blog</description>
	<lastBuildDate>Mon, 30 Mar 2026 16:35:21 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>
	<item>
		<title>Agent Experience Optimisation in Large Code Bases &#8211; Skill vs. Prompt only</title>
		<link>https://ronnieschaniel.com/ai/agent-experience-optimisation-in-large-code-bases-skill-vs-prompt-only/</link>
		
		<dc:creator><![CDATA[ronnieschaniel@hey.com]]></dc:creator>
		<pubDate>Mon, 30 Mar 2026 16:33:09 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<guid isPermaLink="false">https://ronnieschaniel.com/?p=2884</guid>

					<description><![CDATA[<p>Tokens are valuable and so is our time. Let's optimise.</p>
<p>The post <a href="https://ronnieschaniel.com/ai/agent-experience-optimisation-in-large-code-bases-skill-vs-prompt-only/">Agent Experience Optimisation in Large Code Bases &#8211; Skill vs. Prompt only</a> appeared first on <a href="https://ronnieschaniel.com">Ronnie Schaniel</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" width="1024" height="572" src="https://ronnieschaniel.com/wp-content/uploads/2026/03/agent_experience_optimisation_1-1024x572.png" alt="" class="wp-image-2885" srcset="https://ronnieschaniel.com/wp-content/uploads/2026/03/agent_experience_optimisation_1-1024x572.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2026/03/agent_experience_optimisation_1-300x167.png 300w, https://ronnieschaniel.com/wp-content/uploads/2026/03/agent_experience_optimisation_1-768x429.png 768w, https://ronnieschaniel.com/wp-content/uploads/2026/03/agent_experience_optimisation_1.png 1376w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>AI-aided coding is popular. Almost all developers know the feeling when tokens or premium requests run out towards the end of the month. Still lots of devs blindly use their AI agent without much optimisation in mind. We can do better and will see here how to optimise a code base for an AI agent and how much this can save in time and tokens.</p>



<h2 class="wp-block-heading">Agent Experience is the New Dev Experience</h2>



<p>In the previous years code bases were optimised for humans. Good READMEs, nice scripts, a clear structure, verification tests for the code design, etc. Now we have a second actor with AI agents that start to contribute and dominate already in most cases. Agents do not complain much about code bases usually. That means it is on you to optimise it for them. You can still ask an agent what it needs to do its work in an ideal way. Optimising for agent experience will save tokens and time, but it will also improve the quality of your changes and make sure the code base follows a defined structure.</p>



<h2 class="wp-block-heading">Adding an Entity as a concrete Use Case</h2>



<p>In my Java Spring Boot <a href="https://github.com/rschaniel/hexagonal_spring_demo">demo project</a> we will add a new entity in an optimised way.</p>



<figure class="wp-block-image size-full is-resized mt-4"><img decoding="async" width="572" height="314" src="https://ronnieschaniel.com/wp-content/uploads/2026/03/scaffold_entity_skill.png" alt="" class="wp-image-2890" style="width:397px;height:auto" srcset="https://ronnieschaniel.com/wp-content/uploads/2026/03/scaffold_entity_skill.png 572w, https://ronnieschaniel.com/wp-content/uploads/2026/03/scaffold_entity_skill-300x165.png 300w" sizes="(max-width: 572px) 100vw, 572px" /></figure>



<p>In that skill folder we have got references and a script alongside the SKILL.md file. The details of the skill can be found on <a href="https://github.com/rschaniel/hexagonal_spring_demo/tree/main/.github/skills/scaffold-entity" target="_blank" rel="noreferrer noopener">GitHub</a>. Notably the skill creates a spec file and then uses that in scaffold-entity.sh. The script generates multiple files for the entity:</p>



<pre class="wp-block-code"><code lang="bash" class="language-bash">| # | Layer          | File                                |
|---|----------------|-------------------------------------|
| 1 | Domain         | `&lt;Entity&gt;.java`                     |
| 2 | Domain         | `&lt;Entity&gt;Repository.java`           |
| 3 | Application    | `Create&lt;Entity&gt;UseCase.java`        |
| 4 | Application    | `Get&lt;Entity&gt;UseCase.java`           |
| 5 | Application    | `GetAll&lt;Plural&gt;UseCase.java`        |
| 6 | Application    | `Delete&lt;Entity&gt;UseCase.java`        |
| 7 | Application    | `Create&lt;Entity&gt;Command.java`        |
| 8 | Application    | `&lt;Entity&gt;NotFoundException.java`    |
| 9 | API            | `&lt;Entity&gt;Controller.java`           |
| 10| Infrastructure | `&lt;Entity&gt;Entity.java`               |
| 11| Infrastructure | `&lt;Entity&gt;RepositoryAdapter.java`    |
| 12| Infrastructure | `Jpa&lt;Entity&gt;Repository.java`        |
| 13| Test           | `Create&lt;Entity&gt;UseCaseTest.java`    |
| 14| Test           | `&lt;Entity&gt;ControllerTest.java`       |</code></pre>



<p>Like that you receive the complete hexagonal structure for an entity with its basic CRUD operations. Some tests are also introduced.</p>



<p>Compared to a prompt-only approach this is cheaper and faster. The potential savings are summarised in the table below:</p>



<figure class="wp-block-table mt-4"><table class="has-fixed-layout"><thead><tr><th><br></th><th>Without skill (today)</th><th>With skill</th></tr></thead><tbody><tr><td><strong>Agent reads</strong></td><td>5 instruction files + 6-8 reference files (~9,000 input tokens)</td><td>SKILL.md only (~500 input tokens)</td></tr><tr><td><strong>Agent generates</strong></td><td>14 files of boilerplate (~4,000 output tokens)</td><td>1 spec file + 1 shell command (~200 output tokens)</td></tr><tr><td><strong>Tool call overhead</strong></td><td>14&nbsp;<code>create_file</code>&nbsp;calls (~2,800 tokens overhead)</td><td>1&nbsp;<code>create_file</code>&nbsp;+ 1&nbsp;<code>run_in_terminal</code>&nbsp;(~400 tokens)</td></tr><tr><td><strong>Customization</strong></td><td>N/A (interleaved with generation)</td><td>2-3 small edits (~500 output tokens)</td></tr><tr><td><strong>Total tokens</strong></td><td><strong>~16,000-20,000</strong></td><td><strong>~1,600-2,000</strong></td></tr><tr><td><strong>Token savings</strong></td><td>—</td><td><strong>~85-90%</strong></td></tr><tr><td><strong>Wall-clock time</strong></td><td>~3-5 min (many sequential LLM calls)</td><td><strong>~30-45 sec</strong>&nbsp;(one script + small edits)</td></tr><tr><td><strong>Quality</strong></td><td>LLM may deviate from patterns</td><td><strong>Script output is deterministic</strong></td></tr></tbody></table><figcaption class="wp-element-caption"><em>Table by Claude Opus 4.6.</em></figcaption></figure>



<p>You can see that we save 85%-90% in tokens and around the same in time. In addition we make sure that the script creates the same structure every time. A review and a few edits are of course necessary. Let us see in the next section how to further optimise for agent experience.</p>



<h2 class="wp-block-heading">Further Optimisations for AI Coding Agents to Explore</h2>



<p>This is only one way to optimise your coding agent&#8217;s behaviour there are many more aspect. Those I list below.</p>



<ul class="wp-block-list">
<li><strong>Keep a project manifest or architecture summary</strong>&nbsp;— One concise file that tells the agent where things live, what depends on what, and what the conventions are. Without it, every conversation starts with the agent reading half your codebase just to get oriented.</li>



<li><strong>Write scoped instruction files per directory or layer</strong>&nbsp;— Use&nbsp;<code>applyTo</code>&nbsp;patterns so the agent only loads the rules that matter for the file it&#8217;s editing. No need to feed it the entire rulebook when it&#8217;s just touching one corner of the project.</li>



<li><strong>Create prompt files for things you keep asking for</strong>&nbsp;— If you&#8217;ve explained the same task twice, it should be a&nbsp;<code>.prompt.md</code>&nbsp;you invoke with one line. The agent doesn&#8217;t need to rediscover the pattern every time, and you don&#8217;t need to re-type the instructions.</li>



<li><strong>Batch independent reads, sequence dependent ones</strong>&nbsp;— When the agent needs to understand context, let it read multiple files in parallel rather than one by one. But if step B depends on step A&#8217;s result, don&#8217;t let it guess — make that dependency explicit in your instructions. This alone can cut exploration time in half.</li>



<li><strong>Make the agent run tests before it&#8217;s done</strong>&nbsp;— Add a rule that every change ends with a test run. You&#8217;d be surprised how many issues this catches automatically. It&#8217;s cheaper to fail fast in the same conversation than to debug a broken build in the next one.</li>



<li><strong>Be specific in what you ask for</strong>&nbsp;— &#8220;Add a&nbsp;<code>getByStatus</code>&nbsp;method to the Order repository&#8221; costs a fraction of &#8220;make orders filterable by status.&#8221; The more precise the request, the less the agent needs to explore, guess, and over-engineer.</li>



<li><strong>Use a structured change request template</strong>&nbsp;— Three to five lines: what, where, constraints, tests needed. It removes ambiguity completely. The agent stops asking questions and starts working immediately.</li>



<li><strong>Record gotchas and decisions somewhere the agent can find them</strong>&nbsp;— When you hit a tooling quirk or make a design decision that isn&#8217;t obvious from the code, write it down in repo memory. Otherwise the agent will rediscover it the hard way every single time, and you&#8217;ll pay for that investigation in tokens.</li>



<li><strong>Designate a golden reference implementation</strong>&nbsp;— Pick one well-tested vertical slice and treat it as the canonical example. The agent copies from working code instead of interpreting prose rules, which means fewer deviations and more consistency across the board.</li>



<li><strong>Add architecture tests and static analysis to the build</strong>&nbsp;— ArchUnit, ESLint, Checkstyle — whatever fits your stack. Convention violations become build errors, not suggestions the agent might overlook. The feedback loop is instant and doesn&#8217;t depend on anyone reading instructions carefully.</li>
</ul>



<p>Using a coding agent should not just be done blindly, but always with optimisation techniques in mind. Try out different aspects to optimise your coding agent. The good thing is that you can also ask the agent to optimise itself. Make it a habit to regularly update your project for your agent if needed. Agent experience is the new dev experience!</p>
<p>The post <a href="https://ronnieschaniel.com/ai/agent-experience-optimisation-in-large-code-bases-skill-vs-prompt-only/">Agent Experience Optimisation in Large Code Bases &#8211; Skill vs. Prompt only</a> appeared first on <a href="https://ronnieschaniel.com">Ronnie Schaniel</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>A Debug Skill for Your Coding Agent</title>
		<link>https://ronnieschaniel.com/ai/a-debug-skill-for-your-coding-agent/</link>
		
		<dc:creator><![CDATA[ronnieschaniel@hey.com]]></dc:creator>
		<pubDate>Sun, 08 Mar 2026 12:10:05 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<guid isPermaLink="false">https://ronnieschaniel.com/?p=2860</guid>

					<description><![CDATA[<p>Bugs still happen and having AI support to solve them is helpful.</p>
<p>The post <a href="https://ronnieschaniel.com/ai/a-debug-skill-for-your-coding-agent/">A Debug Skill for Your Coding Agent</a> appeared first on <a href="https://ronnieschaniel.com">Ronnie Schaniel</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="571" src="https://ronnieschaniel.com/wp-content/uploads/2026/03/agent_debug_skill-1024x571.png" alt="" class="wp-image-2862" srcset="https://ronnieschaniel.com/wp-content/uploads/2026/03/agent_debug_skill-1024x571.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2026/03/agent_debug_skill-300x167.png 300w, https://ronnieschaniel.com/wp-content/uploads/2026/03/agent_debug_skill-768x428.png 768w, https://ronnieschaniel.com/wp-content/uploads/2026/03/agent_debug_skill.png 1377w" sizes="(max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Generated by Gemini.</figcaption></figure>



<p>Even in the age of AI coding bugs inevitably happen. Some say AI is causing more bugs than we had before. Anyway, also for debugging we can receive some help from AI. That is why I created an Agent Skill for debugging.</p>



<h2 class="wp-block-heading">The Debug Agent Skill</h2>



<p>Our debug skill should take a stack trace or error description as input. Based on that and the project at hand, it should form hypotheses and create tests to reproduce the issue in the code. <a href="https://code.visualstudio.com/docs/copilot/customization/agent-skills" target="_blank" rel="noreferrer noopener">Skills</a> are an independent, shareable and modular way to customise coding assistants. They are meant for repeatable tasks and they are not only instructions but can also involve more context and scripts.</p>



<p>Today we create a skill in a full stack project that consists of a Java Spring Boot backend and a Angular frontend. The two are connected through an API. We create a debug folder in our skills folder and also place the skill file in there:</p>



<p><code>skills/debug/SKILL.md</code></p>



<p>How does the skill file look like?</p>



<pre class="wp-block-code"><code lang="bash" class="language-bash">---
name: debug
description: Analyze pasted stack traces from any environment, form 2-3 concrete hypotheses, visualize likely failure paths, and reproduce the bug with a failing test before fixing code.
---

# Skill: Debug from Stack Trace

## Purpose

Use this skill when a user pastes a stack trace, error log, or production failure excerpt and wants help debugging the issue in this repository.

This skill is optimized for cases where:

- the failure happened in another environment
- only logs or stack traces are available
- the root cause is uncertain
- the bug should be reproduced with a test before changing code

## Core workflow

Follow this sequence strictly:

**Hard stop rule:** if there is no clear stack trace, no concrete error, or the failure mode is still ambiguous, the very first response must stay in diagnosis mode. That means: summarize what is known, state what is unknown, present 2-3 ranked hypotheses, and ask how to continue or what additional evidence is available. Do not edit code, do not write tests, and do not propose a fix until the uncertainty has been reduced enough to justify a reproduction path.

1. **Parse the stack trace**
   - Extract the exception type, message, top application frames, and repeated patterns.
   - Distinguish framework noise from application frames.
   - Identify the most relevant user-code entry points first.
   - If there is no stack trace, then do not guess blindly. Instead, state what is known vs unknown and what minimal additional evidence would reduce uncertainty.
   - If there is a vague symptom but no clear failure signal, do the same: remain in diagnosis mode first.
   - Build some hypotheses as described in step 2 (without the stack trace evidence if needed), clearly label them as speculative, and ask how to continue before doing anything else.
   - You also do not need to run tests if there is no clear evidence that the bug is in the application code. In that case, give bounded debugging guidance or ask for more information.

2. **Build 2-3 hypotheses**
   - Produce 2 or 3 plausible causes only.
   - This step is mandatory before any implementation work whenever the issue is not already clear from evidence.
   - For each hypothesis include:
     - why it matches the trace
     - which files/classes likely participate
     - what evidence would confirm or disprove it
   - Rank them from most likely to least likely.

3. **Show a diagram**
   - Render a Mermaid flowchart of the likely failing path.
   - The diagram should show:
     - triggering API/use case entry point
     - intermediate collaborators
     - the suspected failing branch or null/missing state
     - the thrown exception
  - The file needs to be created in the root of the project such that I can view it.

4. **Choose the smallest reproduction test**
   - Prefer the narrowest test that can reproduce the bug reliably.
   - In this project, use:
     - **JUnit 5 + Mockito** for application/use-case bugs
     - **`@WebMvcTest`** for controller/API behavior bugs
     - **integration-style test only when necessary** for cross-layer behavior that cannot be reproduced in isolation
   - Avoid `@SpringBootTest` unless there is no smaller realistic reproduction.

5. **Write the test first**
   - Add or update a test that reproduces the suspected issue.
   - The test must fail first.
   - State clearly why that failing test reproduces the bug.

6. **Only then fix the code**
   - After the failure is confirmed, implement the smallest safe fix.
   - Preserve architecture boundaries:
     - `api` → `application` → `domain` ← `infrastructure`
   - Do not introduce framework code into domain classes.

7. **Verify after the fix**
   - Re-run the reproduction test.
   - Re-run related tests if needed.
   - Summarize root cause, fix, and why the test now passes.

## Output format

When using this skill, structure the response like this:

If the issue is unclear or there is no clear stack trace, stop after sections 1 and 2 first, then ask for confirmation or more evidence before moving on.

### 1. Observations
- exception type
- important message
- most relevant application frames

### 2. Hypotheses
- Hypothesis 1
- Hypothesis 2
- Hypothesis 3 (optional)

### 3. Failure diagram
- Mermaid flowchart showing the likely failing path

### 4. Reproduction plan
- whether to use a unit, MVC, or integration test
- what exact scenario should fail first

### 5. Fix plan
- smallest likely code change after the test fails

## Hypothesis guidelines

Good hypotheses are:

- specific
- tied to concrete classes or methods
- falsifiable
- based on evidence from the trace

Avoid vague guesses like:

- "something is null somewhere"
- "configuration issue maybe"

Prefer concrete statements like:

- "`GetRaceUseCase.execute()` likely dereferences a missing repository result because the trace ends at `Optional.get()` and the API path is `GET /races/{id}`."

## Diagram template

Use a Mermaid flowchart similar to this:

```mermaid
flowchart TD
    A[HTTP request enters controller] --&gt; B[Controller calls use case]
    B --&gt; C[Use case queries repository]
    C --&gt; D{Expected entity present?}
    D -- No --&gt; E[Missing state / null / empty Optional]
    E --&gt; F[Exception thrown]
    D -- Yes --&gt; G[Normal response]
```

Adjust node labels to the concrete stack trace and code path.

## Test selection guidelines

### Use a unit test when

- the failure is inside a use case
- collaborators can be mocked
- the bug is a null, missing `Optional`, mapping error, or branch error

### Use a controller test when

- the failure depends on HTTP request handling
- status codes, JSON bodies, path variables, or request binding matter

### Use an integration test when

- multiple layers must collaborate to trigger the bug
- persistence or serialization is essential to reproduce it

## Project-specific rules

For this repository:

- application tests should use **JUnit 5 + Mockito**
- controller tests should use **`@WebMvcTest`**
- avoid `@SpringBootTest` for ordinary debugging work
- use domain repository interfaces as mocks, not JPA repositories
- keep use cases single-purpose with one public `execute(...)` method

## If the stack trace is incomplete

If the user provides only a partial trace, still proceed:

- identify the best visible application frame
- state what is known vs unknown
- give 2-3 bounded hypotheses
- suggest the minimal additional evidence that would reduce uncertainty

## Recommended default behavior

For this project, prefer the following order:

1. analyze pasted stack trace or available evidence
2. if the issue is unclear, stop and produce 2-3 hypotheses first
3. only after the failure path is credible, show Mermaid diagram
4. write failing reproduction test
5. fix code
6. verify the test passes

Do not skip the failing-test step unless the user explicitly asks to.
Do not skip the hypotheses-first step when the evidence is weak or ambiguous.</code></pre>



<p>Let us see next, how we can use that skill.</p>



<h2 class="wp-block-heading">Applying the Debug Skill on Different Problems</h2>



<h3 class="wp-block-heading">Easy Bug with Stack Trace</h3>



<p>I&#8217;ve added a &#8220;java.util.NoSuchElementException: No value present&#8221; issue deliberately into our code. And triggered that issue. The resulting stack trace contains the following</p>



<pre class="wp-block-code"><code lang="java" class="language-java">There was an unexpected error (type=Internal Server Error, status=500).
No value present
java.util.NoSuchElementException: No value present
	at java.base/java.util.Optional.get(Optional.java:143)
	at com.ultratrail.application.GetRaceUseCase.execute(GetRaceUseCase.java:16)
	at com.ultratrail.api.RaceController.getRace(RaceController.java:46)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
	at java.base/java.lang.reflect.Method.invoke(Method.java:565)</code></pre>



<p>We can now chat to Copilot and we see below that a skill was read and that skill is the debug one:</p>



<figure class="wp-block-image size-large mt-4"><img decoding="async" width="1024" height="1024" src="https://ronnieschaniel.com/wp-content/uploads/2026/03/debug_skill_activated-1024x1024.png" alt="" class="wp-image-2867" srcset="https://ronnieschaniel.com/wp-content/uploads/2026/03/debug_skill_activated-1024x1024.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2026/03/debug_skill_activated-300x300.png 300w, https://ronnieschaniel.com/wp-content/uploads/2026/03/debug_skill_activated-150x150.png 150w, https://ronnieschaniel.com/wp-content/uploads/2026/03/debug_skill_activated-768x768.png 768w, https://ronnieschaniel.com/wp-content/uploads/2026/03/debug_skill_activated.png 1280w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>It generated a diagram that describes the issue. I believe this is important to review the issue easily. The diagram explains it in a way such that it only takes a few seconds. That helps to keep the understanding of the system alive and spot additional issues that could appear:</p>



<figure class="wp-block-image size-large mt-4"><img decoding="async" width="1024" height="769" src="https://ronnieschaniel.com/wp-content/uploads/2026/03/debug_skill_diagram_explanation-1024x769.png" alt="" class="wp-image-2868" srcset="https://ronnieschaniel.com/wp-content/uploads/2026/03/debug_skill_diagram_explanation-1024x769.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2026/03/debug_skill_diagram_explanation-300x225.png 300w, https://ronnieschaniel.com/wp-content/uploads/2026/03/debug_skill_diagram_explanation-768x577.png 768w, https://ronnieschaniel.com/wp-content/uploads/2026/03/debug_skill_diagram_explanation-1536x1154.png 1536w, https://ronnieschaniel.com/wp-content/uploads/2026/03/debug_skill_diagram_explanation-2048x1538.png 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>Then it continued by creating a test to reproduce the issue. This helps that the issue does not reappear in the future. It also fixed the issue because it is an easy fix:</p>



<figure class="wp-block-image size-large mt-4"><img decoding="async" width="1024" height="142" src="https://ronnieschaniel.com/wp-content/uploads/2026/03/debug_skill_issue_fixed-1024x142.png" alt="" class="wp-image-2870" srcset="https://ronnieschaniel.com/wp-content/uploads/2026/03/debug_skill_issue_fixed-1024x142.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2026/03/debug_skill_issue_fixed-300x42.png 300w, https://ronnieschaniel.com/wp-content/uploads/2026/03/debug_skill_issue_fixed-768x107.png 768w, https://ronnieschaniel.com/wp-content/uploads/2026/03/debug_skill_issue_fixed.png 1382w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>Finally the view in our chat shows that the test was failing first. Then the fix (shown above) was applied and the rerun of the test also proved that the problem is fixed:</p>



<figure class="wp-block-image size-large mt-4"><img decoding="async" width="1024" height="383" src="https://ronnieschaniel.com/wp-content/uploads/2026/03/debug_skill_testing_and_retesting-1024x383.png" alt="" class="wp-image-2869" srcset="https://ronnieschaniel.com/wp-content/uploads/2026/03/debug_skill_testing_and_retesting-1024x383.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2026/03/debug_skill_testing_and_retesting-300x112.png 300w, https://ronnieschaniel.com/wp-content/uploads/2026/03/debug_skill_testing_and_retesting-768x287.png 768w, https://ronnieschaniel.com/wp-content/uploads/2026/03/debug_skill_testing_and_retesting.png 1074w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>This ensures that we don&#8217;t have follow up issues and that the issue was reproduced and (most likely) correctly fixed. If we now would visit http://localhost:8080/api/races/12312 the result is &#8220;Race not found with id: 12312&#8221;.</p>



<h3 class="wp-block-heading">Harder Issues where we Need Hypotheses</h3>



<p>Now we try something harder that does not even have a stack trace:</p>



<p><code>Sometimes I have an issue on Production and the database is not responding with the results I would expect. Please help me. It's in the area of races. Sometimes it looks like there is just no connection to the DB.</code></p>



<p>How will CoPilot handle that?</p>



<figure class="wp-block-image size-large mt-4"><img decoding="async" width="1024" height="734" src="https://ronnieschaniel.com/wp-content/uploads/2026/03/debug_skill_hypotheses-1024x734.png" alt="" class="wp-image-2873" srcset="https://ronnieschaniel.com/wp-content/uploads/2026/03/debug_skill_hypotheses-1024x734.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2026/03/debug_skill_hypotheses-300x215.png 300w, https://ronnieschaniel.com/wp-content/uploads/2026/03/debug_skill_hypotheses-768x550.png 768w, https://ronnieschaniel.com/wp-content/uploads/2026/03/debug_skill_hypotheses-1536x1100.png 1536w, https://ronnieschaniel.com/wp-content/uploads/2026/03/debug_skill_hypotheses.png 1826w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>We see that it helped telling in the <code>SKILL.md</code> that there should be no guesses. Thanks to those hypotheses listed we can deliver more evidence first before aimlessly trying to solve an unclear issue.</p>



<h2 class="wp-block-heading">Consequences of Having a Debug Skill</h2>



<p>For me mainly two aspects were important:</p>



<ol class="wp-block-list numeric-list">
<li>There&#8217;s a well defined debug procedure that allows to use AI.</li>



<li>But on the other hand, AI debugging should be kept inside some boundaries.</li>
</ol>



<p>We all know that LLMs and coding agents try to always find a solution. I have seen coding agents iterating forever to find an issue. With above solution I wanted to prevent this endless loop of stupidly trying to solve and issue that might not even be in the context of the application. That is why the stopping condition is very important. The hypotheses and the collection of more evidence help to pinpoint the issue. </p>



<p>Developers will use AI to debug. That is guaranteed. I still believe that there should be a general understanding of issues. This helps to steer the agent better for further changes. That is why I also wanted a diagram as output. The diagram should explain the problem such that the develop understands in a few seconds what was happening.</p>
<p>The post <a href="https://ronnieschaniel.com/ai/a-debug-skill-for-your-coding-agent/">A Debug Skill for Your Coding Agent</a> appeared first on <a href="https://ronnieschaniel.com">Ronnie Schaniel</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Agent Skills for Hexagonal Architecture in Spring Boot</title>
		<link>https://ronnieschaniel.com/ai/agent-skills-for-hexagonal-architecture-in-spring-boot/</link>
		
		<dc:creator><![CDATA[ronnieschaniel@hey.com]]></dc:creator>
		<pubDate>Sat, 28 Feb 2026 14:54:57 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<guid isPermaLink="false">https://ronnieschaniel.com/?p=2837</guid>

					<description><![CDATA[<p>Define portable, modular, on-demand AI capabilities for specific tasks.</p>
<p>The post <a href="https://ronnieschaniel.com/ai/agent-skills-for-hexagonal-architecture-in-spring-boot/">Agent Skills for Hexagonal Architecture in Spring Boot</a> appeared first on <a href="https://ronnieschaniel.com">Ronnie Schaniel</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="572" src="https://ronnieschaniel.com/wp-content/uploads/2026/02/agent_skills_for_hexagonal_architecture-1024x572.png" alt="" class="wp-image-2838" srcset="https://ronnieschaniel.com/wp-content/uploads/2026/02/agent_skills_for_hexagonal_architecture-1024x572.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2026/02/agent_skills_for_hexagonal_architecture-300x167.png 300w, https://ronnieschaniel.com/wp-content/uploads/2026/02/agent_skills_for_hexagonal_architecture-768x429.png 768w, https://ronnieschaniel.com/wp-content/uploads/2026/02/agent_skills_for_hexagonal_architecture.png 1376w" sizes="(max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Image generated by Gemini</figcaption></figure>



<p>Agent skills are folders of instructions, scripts, and resources. Copilot can load the files when required to fulfil specialised tasks. <a href="https://github.com/agentskills/agentskills" target="_blank" rel="noreferrer noopener">Agentskills</a> is an open standard and therefore should be a good choice for sharing. We will try it out and see how skills can be helpful in a Java Spring Boot project based on hexagonal architecture.</p>



<h2 class="wp-block-heading">What Agent Skills do we need?</h2>



<p>Our demo application is the same that we used in <a href="https://ronnieschaniel.com/ai/hexagonal-architecture-in-spring-boot-projects-with-ai/">Hexagonal Architecture in Spring Boot Projects with AI</a>.  The theme is the one of Ultra Trail races. And the backend currently allows to manage races with their plans, segments, and accommodation. Alongside the Spring backend that offers the DB and the APIs we have a Angular frontend in the same repository.</p>



<p>In such a full-stack mono repo, I can think about the following agent skills:</p>



<ol class="wp-block-list numeric-list">
<li>Transforming the Java DTOs on the backend to TypeScript models on the frontend.</li>



<li>Generating a use case on the backend.</li>



<li>Implementation of a backend HTTP call to a third-party system based on an Open API definition.</li>



<li>Creating a e2e test based on the frontend and the backend mocks.</li>



<li>Retrieve the latest CI/CD log for failures and support in fixing.</li>



<li>Create API tests for the backend based on domain knowledge.</li>
</ol>



<p>Let us explore the first idea in more detail and create a skill for model creation out of DTOs.</p>



<h2 class="wp-block-heading">Creating the Agent Skills</h2>



<p>Because a skill can involved scripts, we will make use of that. Under the <code>.github</code> folder in our repository we create a <code>skills</code> folder and add a first skill folder <code>controller-data-to-ts-models</code>. Now inside that folder we need a SKILL.md file with a defined structure:</p>



<pre class="wp-block-code"><code lang="markdown" class="language-markdown">---
name: controller-data-to-ts-models
description: Generate TypeScript model files from data contracts exposed by Spring controllers (@RequestBody and ResponseEntity&lt;T&gt;). Use this when asked to keep frontend models aligned with API controller contracts.
---

# Skill: Controller Data → TypeScript Models

## Purpose

Generate frontend TypeScript model files from the data contracts exposed by Spring controllers:

- request payload types from `@RequestBody`
- response payload types from `ResponseEntity&lt;T&gt;`

This keeps Angular models aligned with API contracts instead of generating from every domain class.

## Scope

The generator scans:

- `src/main/java/com/ultratrail/api/*Controller.java`

It resolves and emits models for referenced classes from:

- `src/main/java/com/ultratrail/application/*Command.java`
- `src/main/java/com/ultratrail/domain/*.java`

## Output

Generated files are written to:

- `frontend/src/app/models/*.model.ts`

If a model file already exists with the same name, it is overwritten.

## Run

From project root:

```bash
node .github/skills/controller-data-to-ts-models/generate-controller-ts-models.mjs
```

Or from any directory inside the repository:

```bash
node /absolute/path/to/repo/.github/skills/controller-data-to-ts-models/generate-controller-ts-models.mjs
```

## Mapping rules

- `String`, `LocalDate`, `LocalDateTime`, `Instant`, `UUID` → `string`
- `Integer`, `Long`, `Double`, `Float`, `BigDecimal` → `number`
- `Boolean` → `boolean`
- `List&lt;T&gt;`, `Set&lt;T&gt;`, `Collection&lt;T&gt;` → `T[]`
- Java nested enums (e.g., `Race.RaceStatus`) are emitted as TS union types.
- Field `id` is generated as optional (`id?: ...`).

## Notes

- The generator is intentionally contract-focused and does not modify existing handcrafted models.
- If you change controller request/response types, rerun the generator to refresh the generated files.</code></pre>



<p>And alongside the standard file follows our script <code>generate-controller-ts-models.mjs</code>. Here just some extracts of it such that you get the basic understanding:</p>



<pre class="wp-block-code"><code lang="javascript" class="language-javascript">...

function listControllerFiles() {
  if (!fs.existsSync(apiDir)) return [];
  return fs
    .readdirSync(apiDir)
    .filter((file) =&gt; file.endsWith('Controller.java'))
    .map((file) =&gt; path.join(apiDir, file));
}

... 

function parseFields(content) {
  const fields = [];
  const fieldRegex = /private\s+([A-Za-z0-9_$.&lt;&gt;]+)\s+([A-Za-z0-9_]+)\s*;/g;
  let match;
  while ((match = fieldRegex.exec(content)) !== null) {
    fields.push({
      javaType: sanitizeType(match[1]),
      name: match[2],
    });
  }
  return fields;
}</code></pre>



<p>This is the skill. And now we need to see how we can use it.</p>



<h2 class="wp-block-heading">Make CoPilot use the new Agent Skill</h2>



<p>First of all you need to make sure that CoPilot skills are enabled in your IDE. I&#8217;m taking vscode as an example and you can search for &#8220;skills&#8221; in the settings and tick the checkbox:</p>



<figure class="wp-block-image size-large mt-4"><img decoding="async" width="1024" height="770" src="https://ronnieschaniel.com/wp-content/uploads/2026/02/vscode_agent_skills_settings-1024x770.png" alt="Screenshot showing where to enable agent skills in VC code." class="wp-image-2841" srcset="https://ronnieschaniel.com/wp-content/uploads/2026/02/vscode_agent_skills_settings-1024x770.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2026/02/vscode_agent_skills_settings-300x225.png 300w, https://ronnieschaniel.com/wp-content/uploads/2026/02/vscode_agent_skills_settings-768x577.png 768w, https://ronnieschaniel.com/wp-content/uploads/2026/02/vscode_agent_skills_settings-1536x1154.png 1536w, https://ronnieschaniel.com/wp-content/uploads/2026/02/vscode_agent_skills_settings.png 1578w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>Then we are ready to go. Let us have a look at the AccommodationController.java where we find various endpoints using DTOs:</p>



<pre class="wp-block-code"><code lang="java" class="language-java">@GetMapping
public ResponseEntity&lt;List&lt;Accommodation&gt;&gt; getAccommodations(@PathVariable String raceId) {
    return ResponseEntity.ok(getAccommodationsByRaceUseCase.execute(raceId));
}</code></pre>



<p>In this case here, the DTO would be <code>Accommodation</code> and it looks as follows:</p>



<pre class="wp-block-code"><code lang="java" class="language-java">@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Accommodation {
    private String id;
    private String raceId;
    private String name;
    private String location;
    private AccommodationType type;
    private LocalDate checkIn;
    private LocalDate checkOut;
    private Boolean breakfastIncluded;
    private BigDecimal pricePerNight;
    private BigDecimal distanceToStartKm;
    private BigDecimal distanceToFinishKm;
    private RoomType roomType;
    private Integer numberOfGuests;

    public enum AccommodationType {
        RACE_START, RACE_FINISH, FULL_STAY
    }

    public enum RoomType {
        SINGLE, DOUBLE
    }
}</code></pre>



<p>Now over to Copilot:</p>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="502" src="https://ronnieschaniel.com/wp-content/uploads/2026/02/vscode_agent_skill_found-1024x502.png" alt="" class="wp-image-2842" srcset="https://ronnieschaniel.com/wp-content/uploads/2026/02/vscode_agent_skill_found-1024x502.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2026/02/vscode_agent_skill_found-300x147.png 300w, https://ronnieschaniel.com/wp-content/uploads/2026/02/vscode_agent_skill_found-768x377.png 768w, https://ronnieschaniel.com/wp-content/uploads/2026/02/vscode_agent_skill_found.png 1130w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>I click &#8220;Allow&#8221; and the script&#8217;s skill starts to do its work and reports back after only a few seconds. In my case it updated all the models which is okay:</p>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="233" src="https://ronnieschaniel.com/wp-content/uploads/2026/02/vscode_agent_skill_done-1024x233.png" alt="" class="wp-image-2843" srcset="https://ronnieschaniel.com/wp-content/uploads/2026/02/vscode_agent_skill_done-1024x233.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2026/02/vscode_agent_skill_done-300x68.png 300w, https://ronnieschaniel.com/wp-content/uploads/2026/02/vscode_agent_skill_done-768x174.png 768w, https://ronnieschaniel.com/wp-content/uploads/2026/02/vscode_agent_skill_done.png 1118w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>This gives us the following model in the frontend for the Accommodation interface:</p>



<pre class="wp-block-code"><code lang="typescript" class="language-typescript">export interface Accommodation {
  id?: string;
  raceId: string;
  name: string;
  location: string;
  type: AccommodationType;
  checkIn: string;
  checkOut: string;
  breakfastIncluded: boolean;
  pricePerNight: number;
  distanceToStartKm: number;
  distanceToFinishKm: number;
  roomType: RoomType;
  numberOfGuests: number;
}

export type AccommodationType = 'RACE_START' | 'RACE_FINISH' | 'FULL_STAY';
export type RoomType = 'SINGLE' | 'DOUBLE';</code></pre>



<p>We see that the types were mapped correctly.</p>



<h2 class="wp-block-heading">What are the Differences to Instructions, Prompts and Custom Agents?</h2>



<p>Agent Skills are modular, on-demand, and portable AI capabilities containing specific instructions and tools, whereas custom instructions are persistent, general-purpose directives that apply to every interaction. In my experience agent skills are a great combination of well defined scripts and the power of an LLM. You have the determinism of a script that gives you more safety. Other than that, it&#8217;s also cheaper to execute the scripts than to really on AI to do the transformation. Prompts are on-demand commands executed by typing, e.g. <code>/review</code>, for a review prompt. They are made for repeatable one-shot functionalities.</p>



<p>If we would like to compare all of it, the following table can help:</p>



<figure class="wp-block-table mt-4"><table class="has-fixed-layout"><tbody><tr><td><strong>Concept</strong></td><td><strong>Description</strong></td><td><strong>Best used for</strong></td></tr><tr><td><a href="https://docs.github.com/en/copilot/tutorials/customization-library/custom-instructions" target="_blank" rel="noreferrer noopener">Instructions</a></td><td>Always on, standards and default behaviours</td><td>Coding standards, team preferences, broad context</td></tr><tr><td><a href="https://docs.github.com/en/copilot/tutorials/customization-library/prompt-files" target="_blank" rel="noreferrer noopener">Prompts</a></td><td>A manual and on-demand execution of a prompt</td><td>A simple task that does not need any loop</td></tr><tr><td><a href="https://docs.github.com/en/copilot/concepts/agents/about-agent-skills" target="_blank" rel="noreferrer noopener">Skills</a></td><td>Combination of instructions, scripts and resources that are loaded when needed</td><td>Detailed instructions for a very specific task that requires more than just a simple prompt</td></tr><tr><td><a href="https://docs.github.com/en/copilot/concepts/agents/coding-agent/about-custom-agents" target="_blank" rel="noreferrer noopener">Custom agents</a></td><td>Agents for specific development tasks that can use tools and MCP</td><td>Used if you need to tailor the coding agent to your unique workflows, coding conventions, and use cases</td></tr></tbody></table></figure>



<p>Best is if you try out the various possibilities you have. And remember that you can always let AI itself create the skills, prompts, instructions and agents.</p>



<p><sup><em>All my articles are written by myself. AI is used for some of the title images and in some cases to improve the wording and flow.</em></sup></p>
<p>The post <a href="https://ronnieschaniel.com/ai/agent-skills-for-hexagonal-architecture-in-spring-boot/">Agent Skills for Hexagonal Architecture in Spring Boot</a> appeared first on <a href="https://ronnieschaniel.com">Ronnie Schaniel</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Hexagonal Architecture in Spring Boot Projects with AI</title>
		<link>https://ronnieschaniel.com/ai/hexagonal-architecture-in-spring-boot-projects-with-ai/</link>
		
		<dc:creator><![CDATA[ronnieschaniel@hey.com]]></dc:creator>
		<pubDate>Tue, 24 Feb 2026 19:31:52 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<guid isPermaLink="false">https://ronnieschaniel.com/?p=2797</guid>

					<description><![CDATA[<p>How to give coding agents boundaries by adding ArchUnit tests and instruction files.</p>
<p>The post <a href="https://ronnieschaniel.com/ai/hexagonal-architecture-in-spring-boot-projects-with-ai/">Hexagonal Architecture in Spring Boot Projects with AI</a> appeared first on <a href="https://ronnieschaniel.com">Ronnie Schaniel</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="572" src="https://ronnieschaniel.com/wp-content/uploads/2026/02/hexagonal_architecture_in_spring_boot_projects_with_AI-1024x572.png" alt="" class="wp-image-2799" srcset="https://ronnieschaniel.com/wp-content/uploads/2026/02/hexagonal_architecture_in_spring_boot_projects_with_AI-1024x572.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2026/02/hexagonal_architecture_in_spring_boot_projects_with_AI-300x167.png 300w, https://ronnieschaniel.com/wp-content/uploads/2026/02/hexagonal_architecture_in_spring_boot_projects_with_AI-768x429.png 768w, https://ronnieschaniel.com/wp-content/uploads/2026/02/hexagonal_architecture_in_spring_boot_projects_with_AI.png 1376w" sizes="(max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Generated by Gemini</figcaption></figure>



<p>The advent of AI has revolutionised the way software engineers approach programming. While we can now leverage code generation in various ways, I firmly believe that structure remains essential! AI excels at recognising patterns and working consistently when given guidance. Moreover, the abstraction level has increased, allowing developers to focus more on business logic as well as overall architecture and less on implementation details. Given this, combining AI-generated code with hexagonal architecture should be an intriguing combination. In this article, we will delve into this concept in the context of a full-stack application, specifically focusing on an ultra-running theme (races longer than 42.2km or 26 miles).</p>



<h2 class="wp-block-heading">The Application and its Hexagonal Structure</h2>



<p>Our application is designed to manage races and related athlete information. The main view provides an overview of upcoming events, as depicted in the image below:</p>



<figure class="wp-block-image size-large mt-4"><img decoding="async" width="1024" height="187" src="https://ronnieschaniel.com/wp-content/uploads/2026/02/races_overview-1024x187.png" alt="" class="wp-image-2827" srcset="https://ronnieschaniel.com/wp-content/uploads/2026/02/races_overview-1024x187.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2026/02/races_overview-300x55.png 300w, https://ronnieschaniel.com/wp-content/uploads/2026/02/races_overview-768x140.png 768w, https://ronnieschaniel.com/wp-content/uploads/2026/02/races_overview-1536x280.png 1536w, https://ronnieschaniel.com/wp-content/uploads/2026/02/races_overview-2048x374.png 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>On the detail view of a specific race, users can edit attributes, add segments, and input other relevant details, as shown in the next screenshot:</p>



<figure class="wp-block-image size-large mt-4"><img decoding="async" width="1024" height="733" src="https://ronnieschaniel.com/wp-content/uploads/2026/02/race_detail_view-1024x733.png" alt="" class="wp-image-2828" srcset="https://ronnieschaniel.com/wp-content/uploads/2026/02/race_detail_view-1024x733.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2026/02/race_detail_view-300x215.png 300w, https://ronnieschaniel.com/wp-content/uploads/2026/02/race_detail_view-768x549.png 768w, https://ronnieschaniel.com/wp-content/uploads/2026/02/race_detail_view-1536x1099.png 1536w, https://ronnieschaniel.com/wp-content/uploads/2026/02/race_detail_view-2048x1465.png 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>Now, let&#8217;s take a more technical look at our application. At its core lies the principle of hexagonal, onion, or clean architecture, which dictates that everything revolves around the domain. In this context, infrastructural aspects such as storage and API exposure are dependent on the domain, rather than the other way around. Use cases complete the picture by describing what happens within the domain.</p>



<p>The following diagram provides a visual representation of our hexagonal structure, highlighting a single use case for the Race domain as an example:</p>



<figure class="wp-block-image size-large mt-4"><img decoding="async" width="1024" height="701" src="https://ronnieschaniel.com/wp-content/uploads/2026/02/hexagonal_architecture_structure-1024x701.png" alt="" class="wp-image-2803" srcset="https://ronnieschaniel.com/wp-content/uploads/2026/02/hexagonal_architecture_structure-1024x701.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2026/02/hexagonal_architecture_structure-300x206.png 300w, https://ronnieschaniel.com/wp-content/uploads/2026/02/hexagonal_architecture_structure-768x526.png 768w, https://ronnieschaniel.com/wp-content/uploads/2026/02/hexagonal_architecture_structure-1536x1052.png 1536w, https://ronnieschaniel.com/wp-content/uploads/2026/02/hexagonal_architecture_structure-2048x1403.png 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>In the next section, we&#8217;ll explore how AI coding agents can be informed about the desired structure and important details, enabling seamless integration with our application.</p>



<h2 class="wp-block-heading">Defining Guardrails for AI with ArchUnit and Instruction Files</h2>



<p>To ensure our architecture remains consistent, we&#8217;ll employ <a href="https://www.archunit.org/" target="_blank" rel="noreferrer noopener">ArchUnit</a>, a Java library designed to unit test architectural structures. This approach provides guardrails that help maintain the desired structure. Additionally, we&#8217;ll create instruction files for GitHub CoPilot to specify the preferred Java code style.</p>



<h3 class="wp-block-heading">Describing the Application Architecture with ArchUnit</h3>



<p>We will write tests in five areas to ensure our architecture is well-defined:</p>



<ol class="wp-block-list">
<li><strong>Dependency Allowance</strong>: Verifying the allowed dependencies between the domain, API, infrastructure, and application layers.</li>



<li><strong>Boundary Checks:</strong> Additional checks for boundaries not covered by the initial architecture test (e.g., library usage).</li>



<li><strong>Naming Conventions:</strong> Enforcing consistent naming conventions throughout the codebase.</li>



<li><strong>Structural Rules for Classes:</strong> Validating class structure rules to maintain a cohesive architecture.</li>



<li><strong>Circular Dependency Check:</strong> Ensuring that dependencies between application parts are respected and follow the desired design without circles.</li>
</ol>



<p>Below is the most important check that ensures the dependencies rules between the different parts of the application are respected:</p>



<pre class="wp-block-code"><code lang="java" class="language-java">@ArchTest
static final ArchRule layerDependenciesAreRespected =
    layeredArchitecture()
        .consideringAllDependencies()
        .layer("Domain").definedBy("com.ultratrail.domain..")
        .layer("Application").definedBy("com.ultratrail.application..")
        .layer("API").definedBy("com.ultratrail.api..")
        .layer("Infrastructure").definedBy("com.ultratrail.infrastructure..")

        .whereLayer("Domain").mayOnlyBeAccessedByLayers("Application", "API", "Infrastructure")
        .whereLayer("Application").mayOnlyBeAccessedByLayers("API")
        .whereLayer("Infrastructure").mayNotBeAccessedByAnyLayer()
        .whereLayer("API").mayNotBeAccessedByAnyLayer();</code></pre>



<p>To begin, we define the packages for each layer: Domain, Application, API, and Infrastructure. This serves as the foundation for our architecture, allowing us to identify the distinct layers.<br>Next, we determine which layers may be accessed by others. For instance, the Domain layer should not access other layers directly, ensuring a clear separation of concerns. By defining these boundaries, we establish the guardrails that govern our application&#8217;s architecture.</p>



<p>For additional boundaries, we add another test:</p>



<pre class="wp-block-code"><code lang="java" class="language-java">@ArchTest
static final ArchRule domainMustBeFrameworkFree =
    noClasses().that().resideInAPackage("com.ultratrail.domain..")
        .should().dependOnClassesThat()
        .resideInAnyPackage(
                "org.springframework..",
                "jakarta.persistence..",
                "javax.persistence.."
        )
        .because("The domain layer must not depend on any framework.");

@ArchTest
static final ArchRule applicationMustNotUseJpa =
    noClasses().that().resideInAPackage("com.ultratrail.application..")
        .should().dependOnClassesThat()
        .resideInAnyPackage("jakarta.persistence..", "javax.persistence..")
        .because("The application layer must not use JPA directly.");

@ArchTest
static final ArchRule applicationMustNotUseSpringMvc =
    noClasses().that().resideInAPackage("com.ultratrail.application..")
        .should().dependOnClassesThat()
        .resideInAnyPackage("org.springframework.web..", "jakarta.servlet..")
        .because("The application layer must not depend on Spring MVC or Servlet API.");

@ArchTest
static final ArchRule apiMustNotUseJpa =
    noClasses().that().resideInAPackage("com.ultratrail.api..")
        .should().dependOnClassesThat()
        .resideInAnyPackage("jakarta.persistence..", "javax.persistence..")
        .because("The API layer must not use JPA directly.");</code></pre>



<p>A third test covers naming conventions, ensuring consistency and clarity in our codebase:</p>



<pre class="wp-block-code"><code lang="java" class="language-java">@ArchTest
static final ArchRule useCasesMustBeNamedCorrectly =
    classes().that().resideInAPackage("com.ultratrail.application..")
        .and().areAnnotatedWith(org.springframework.stereotype.Service.class)
        .should().haveSimpleNameEndingWith("UseCase")
        .because("All @Service classes in the application layer must be named *UseCase.");

@ArchTest
static final ArchRule commandsMustBeNamedCorrectly =
    classes().that().resideInAPackage("com.ultratrail.application..")
        .and().areNotAnnotatedWith(org.springframework.stereotype.Service.class)
        .and().areNotAssignableTo(RuntimeException.class)
        .and().areNotInterfaces()
        .should().haveSimpleNameEndingWith("Command")
        .because("Non-use-case, non-exception classes in the application layer must be named *Command.");

@ArchTest
static final ArchRule exceptionsMustBeNamedCorrectly =
    classes().that().resideInAPackage("com.ultratrail.application..")
        .and().areAssignableTo(RuntimeException.class)
        .should().haveSimpleNameEndingWith("NotFoundException")
        .because("Domain exceptions in the application layer must be named *NotFoundException.");

@ArchTest
static final ArchRule controllersMustBeNamedCorrectly =
    classes().that().resideInAPackage("com.ultratrail.api..")
        .and().areAnnotatedWith(org.springframework.web.bind.annotation.RestController.class)
        .should().haveSimpleNameEndingWith("Controller")
        .because("All REST controllers must be named *Controller.");

@ArchTest
static final ArchRule repositoryAdaptersMustBeNamedCorrectly =
    classes().that().resideInAPackage("com.ultratrail.infrastructure..")
        .and().areAnnotatedWith(org.springframework.stereotype.Component.class)
        .and().areNotInterfaces()
        .should().haveSimpleNameEndingWith("RepositoryAdapter")
        .because("Infrastructure adapters must be named *RepositoryAdapter.");

@ArchTest
static final ArchRule jpaEntitiesMustBeNamedCorrectly =
    classes().that().resideInAPackage("com.ultratrail.infrastructure..")
        .and().areAnnotatedWith(jakarta.persistence.Entity.class)
        .should().haveSimpleNameEndingWith("Entity")
        .because("JPA entity classes must be named *Entity.");</code></pre>



<p>Additionally, more structural rules are defined below to further refine our architectural design.</p>



<pre class="wp-block-code"><code lang="java" class="language-java">@ArchTest
static final ArchRule useCaseFieldsMustBeFinal =
    classes().that().resideInAPackage("com.ultratrail.application..")
        .and().haveSimpleNameEndingWith("UseCase")
        .should().haveOnlyFinalFields()
        .because("Use case classes must have only final (injected) fields — no mutable state.");

@ArchTest
static final ArchRule noFieldInjectionAnywhere =
    noFields().that().areDeclaredInClassesThat()
        .resideInAnyPackage(
                "com.ultratrail.domain..",
                "com.ultratrail.application..",
                "com.ultratrail.api..",
                "com.ultratrail.infrastructure.."
        )
        .should().beAnnotatedWith(org.springframework.beans.factory.annotation.Autowired.class)
        .because("Field injection with @Autowired is forbidden. Use constructor injection.");

@ArchTest
static final ArchRule controllersMustNotDependOnRepositories =
    noClasses().that().resideInAPackage("com.ultratrail.api..")
        .and().areAnnotatedWith(org.springframework.web.bind.annotation.RestController.class)
        .should().dependOnClassesThat()
        .haveSimpleNameEndingWith("Repository")
        .because("Controllers must not directly depend on repository interfaces — inject use cases instead.");</code></pre>



<p>And finally, we ensure that no circular dependencies occur, guaranteeing the stability and maintainability of our architecture.</p>



<pre class="wp-block-code"><code lang="java" class="language-java">@ArchTest
static final ArchRule noCircularDependenciesBetweenLayers =
    slices().matching("com.ultratrail.(*)..").should().beFreeOfCycles()
        .because("There must be no circular dependencies between architecture layers.");</code></pre>



<p>With these ArchUnit tests in place, our AI coding agents are provided with clear boundaries to operate within. Now, let&#8217;s explore further steering possibilities through instruction files.</p>



<h3 class="wp-block-heading">Creating Instruction Files to Guide GitHub CoPilot</h3>



<p><a href="https://docs.github.com/en/copilot/how-tos/configure-custom-instructions/add-repository-instructions" target="_blank" rel="noreferrer noopener">Instruction files</a> play a crucial role in providing context and guidance for your AI coding agents. By placing these files in the&nbsp;<code>.github/instructions</code>&nbsp;directory, you can help your agent understand how to build, test, and validate changes.</p>



<p>Let&#8217;s begin by creating instructions for the core of our hexagonal architecture – the Domain. This will be defined in a&nbsp;<code>domain.instructions.md</code>&nbsp;file:</p>



<pre class="wp-block-code"><code lang="markdown" class="language-markdown">---
applyTo: "src/main/java/com/ultratrail/domain/**"
---

# Domain Layer Rules

You are generating code in the **domain layer** (`com.ultratrail.domain`).
This layer is the core of the hexagonal architecture and must remain 100 % framework-free.

## Strict Rules

### Allowed imports
- `java.*`
- `lombok.*`
- Other `com.ultratrail.domain.*` classes

### Forbidden imports – NEVER add these
- `org.springframework.*`
- `jakarta.persistence.*` / `javax.persistence.*`
- `com.ultratrail.application.*`
- `com.ultratrail.api.*`
- `com.ultratrail.infrastructure.*`

### Domain Entities
- Annotate every entity with `@Data`, `@Builder`, `@NoArgsConstructor`, `@AllArgsConstructor` (Lombok).
- All IDs are `String` (UUID format).
- Do **not** add `@Entity`, `@Table`, `@Column`, or any JPA annotation.
- Embed domain-specific enums as static nested enums inside the owning entity.
- Do not add Spring bean annotations (`@Service`, `@Component`, `@Repository`).
- No business logic methods that depend on external services.

### Repository Interfaces (Ports)
- Repository interfaces are pure Java interfaces — they define the port, not the implementation.
- Name them `&lt;Entity&gt;Repository` (e.g., `RaceRepository`).
- Only use domain types as parameter and return types.
- Common methods: `save`, `findById`, `findAll`, `delete`. Add domain-specific finders as needed.

## Example

```java
package com.ultratrail.domain;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Race {
    private String id;
    private String name;

    public enum RaceStatus { PLANNED, REGISTERED, COMPLETED, DNF, DNS }
}
```

```java
package com.ultratrail.domain;

import java.util.List;
import java.util.Optional;

public interface RaceRepository {
    Race save(Race race);
    Optional&lt;Race&gt; findById(String id);
    List&lt;Race&gt; findAll();
    void delete(String id);
}
```</code></pre>



<p>The Application part is described in&nbsp;<code>application.instructions.md</code>:</p>



<pre class="wp-block-code"><code lang="markdown" class="language-markdown">---
applyTo: "src/main/java/com/ultratrail/application/**"
---

# Application Layer Rules

You are generating code in the **application layer** (`com.ultratrail.application`).
This layer orchestrates use cases and acts as the bridge between the API and the domain.

## Strict Rules

### Allowed imports
- `com.ultratrail.domain.*`
- `org.springframework.stereotype.Service` (use cases only)
- `lombok.*`
- `java.*`

### Forbidden imports – NEVER add these
- `com.ultratrail.api.*`
- `com.ultratrail.infrastructure.*`
- `jakarta.persistence.*` / `javax.persistence.*`
- `org.springframework.web.*`
- `jakarta.servlet.*`
- Any Spring MVC annotation (`@RestController`, `@RequestMapping`, etc.)

---

## Use Cases

- Each use case is a **single class with one public method** named `execute(...)`.
- Annotate with `@Service` and `@AllArgsConstructor` — no other Spring annotations.
- Use constructor injection only. Never use `@Autowired` on fields.
- Inject domain repository interfaces (ports), never JPA repositories.
- Generate new IDs with `UUID.randomUUID().toString()`.
- Naming: `&lt;Verb&gt;&lt;Entity&gt;UseCase` (e.g., `CreateRaceUseCase`, `UpdateRaceStatusUseCase`).

```java
@Service
@AllArgsConstructor
public class CreateRaceUseCase {
    private final RaceRepository raceRepository;

    public Race execute(CreateRaceCommand command) {
        Race race = Race.builder()
                .id(UUID.randomUUID().toString())
                .name(command.getName())
                // ... map remaining fields
                .build();
        return raceRepository.save(race);
    }
}
```

---

## Commands

- Commands are plain data holders with **no business logic**.
- Annotate with `@Data`, `@NoArgsConstructor`, `@AllArgsConstructor` (Lombok).
- No validation annotations unless explicitly requested.
- Naming: `&lt;Verb&gt;&lt;Entity&gt;Command` (e.g., `CreateRaceCommand`, `AddSegmentCommand`).

```java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreateRaceCommand {
    private String name;
    private String location;
}
```

---

## Domain Exceptions

- Extend `RuntimeException`.
- Belong in this package (not in domain).
- Naming: `&lt;Entity&gt;NotFoundException` (e.g., `RaceNotFoundException`, `RacePlanNotFoundException`).

```java
public class RaceNotFoundException extends RuntimeException {
    public RaceNotFoundException(String id) {
        super("Race not found with id: " + id);
    }
}
```</code></pre>



<p>Yet another important aspect is defined in&nbsp;<code>infrastructure.instructions.md</code>.</p>



<pre class="wp-block-code"><code lang="markdown" class="language-markdown">---
applyTo: "src/main/java/com/ultratrail/infrastructure/**"
---

# Infrastructure Layer Rules

You are generating code in the **infrastructure layer** (`com.ultratrail.infrastructure`).
This layer is the outbound adapter: it implements domain ports using Spring Data JPA.

## Strict Rules

### Allowed imports
- `com.ultratrail.domain.*` (to implement repository ports)
- `org.springframework.data.jpa.repository.*`
- `org.springframework.stereotype.Component`
- `jakarta.persistence.*`
- `lombok.*`
- `java.*`

### Forbidden imports – NEVER add these
- `com.ultratrail.api.*`
- `com.ultratrail.application.*`
- `org.springframework.stereotype.Service`
- `org.springframework.web.*`

---

## Repository Adapters

- Annotate with `@Component` and `@AllArgsConstructor`.
- Each adapter **implements exactly one domain repository interface**.
- Delegate every method to a Spring Data JPA repository.
- Convert between domain objects and JPA entities using `fromDomain()` and `toDomain()`.
- JPA entities must **never** appear in method signatures exposed to other layers.
- Naming: `&lt;Entity&gt;RepositoryAdapter` (e.g., `RaceRepositoryAdapter`).

```java
@Component
@AllArgsConstructor
public class RaceRepositoryAdapter implements RaceRepository {
    private final JpaRaceRepository jpaRaceRepository;

    @Override
    public Race save(Race race) {
        return jpaRaceRepository.save(RaceEntity.fromDomain(race)).toDomain();
    }

    @Override
    public Optional&lt;Race&gt; findById(String id) {
        return jpaRaceRepository.findById(id).map(RaceEntity::toDomain);
    }

    @Override
    public List&lt;Race&gt; findAll() {
        return jpaRaceRepository.findAll().stream()
                .map(RaceEntity::toDomain)
                .collect(Collectors.toList());
    }

    @Override
    public void delete(String id) {
        jpaRaceRepository.deleteById(id);
    }
}
```

---

## Spring Data JPA Repositories

- Interface extending `JpaRepository&lt;Entity, String&gt;`.
- Naming: `Jpa&lt;Entity&gt;Repository` (e.g., `JpaRaceRepository`).
- Only add custom query methods as needed.

```java
public interface JpaRaceRepository extends JpaRepository&lt;RaceEntity, String&gt; {
    List&lt;RaceEntity&gt; findByStatus(Race.RaceStatus status);
}
```

---

## JPA Entities

- Annotate with `@Entity`, `@Table`, `@Data`, `@Builder`, `@NoArgsConstructor`, `@AllArgsConstructor`.
- Must include `fromDomain(Domain d)` static factory method and `toDomain()` instance method.
- JPA entities stay confined to this package — never returned from use cases or controllers.
- Naming: `&lt;Entity&gt;Entity` (e.g., `RaceEntity`).

```java
@Entity
@Table(name = "races")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RaceEntity {
    @Id
    private String id;

    @Column(nullable = false)
    private String name;

    public static RaceEntity fromDomain(Race race) {
        return RaceEntity.builder()
                .id(race.getId())
                .name(race.getName())
                .build();
    }

    public Race toDomain() {
        return Race.builder()
                .id(this.id)
                .name(this.name)
                .build();
    }
}
```</code></pre>



<p>The API exposes our functionality as RESTful HTTP endpoints, described in&nbsp;<code>api.instructions.md</code>.</p>



<pre class="wp-block-code"><code lang="markdown" class="language-markdown">---
applyTo: "src/main/java/com/ultratrail/api/**"
---

# API Layer Rules

You are generating code in the **API layer** (`com.ultratrail.api`).
This layer is the inbound adapter: it receives HTTP requests and delegates work to use cases.

## Strict Rules

### Allowed imports
- `com.ultratrail.application.*` (use cases and commands)
- `com.ultratrail.domain.*` (domain entities used only as response types)
- `org.springframework.web.*`
- `org.springframework.http.*`
- `lombok.AllArgsConstructor`
- `java.*`

### Forbidden imports – NEVER add these
- `com.ultratrail.infrastructure.*`
- `jakarta.persistence.*` / `javax.persistence.*`
- Any JPA repository interface
- `org.springframework.stereotype.Service`

---

## Controllers

- Annotate with `@RestController`, `@RequestMapping`, `@AllArgsConstructor`.
- Inject **only use cases** — never repositories, domain services, or infrastructure classes.
- Every HTTP method calls exactly one use case via `.execute(...)`.
- Return `ResponseEntity&lt;T&gt;` for every handler method.
- No business logic inside controllers. Mapping between HTTP and domain happens only by passing commands.
- Use `@PathVariable`, `@RequestBody`, `@RequestParam` for input binding.
- Naming: `&lt;Entity&gt;Controller` (e.g., `RaceController`, `RaceSegmentController`).

```java
@RestController
@RequestMapping("/races")
@AllArgsConstructor
public class RaceController {
    private final CreateRaceUseCase createRaceUseCase;
    private final GetRaceUseCase getRaceUseCase;
    private final DeleteRaceUseCase deleteRaceUseCase;

    @PostMapping
    public ResponseEntity&lt;Race&gt; createRace(@RequestBody CreateRaceCommand command) {
        return ResponseEntity.status(HttpStatus.CREATED).body(createRaceUseCase.execute(command));
    }

    @GetMapping("/{id}")
    public ResponseEntity&lt;Race&gt; getRace(@PathVariable String id) {
        return getRaceUseCase.execute(id)
                .map(ResponseEntity::ok)
                .orElseGet(() -&gt; ResponseEntity.notFound().build());
    }

    @DeleteMapping("/{id}")
    public ResponseEntity&lt;Void&gt; deleteRace(@PathVariable String id) {
        deleteRaceUseCase.execute(id);
        return ResponseEntity.noContent().build();
    }
}
```

---

## Controller Tests

Use `@WebMvcTest` with mocked use cases. Never load the full application context for controller tests.

```java
@WebMvcTest(RaceController.class)
class RaceControllerTest {
    @Autowired MockMvc mockMvc;
    @MockBean CreateRaceUseCase createRaceUseCase;
    // ...
}
```</code></pre>



<p>Besides all the application layers, we also define a testing aspect, which is described in&nbsp;<code>testing.instructions.md</code>.</p>



<pre class="wp-block-code"><code lang="markdown" class="language-markdown">---
applyTo: "src/test/java/com/ultratrail/**"
---

# Testing Rules

You are generating test code for the Ultra Trail Manager project.
Follow these rules to ensure tests are focused, fast, and do not violate the hexagonal architecture.

---

## Use Case Tests (Application Layer)

- Use **plain JUnit 5 + Mockito** — no Spring context.
- Annotate with `@ExtendWith(MockitoExtension.class)`.
- Mock domain repository interfaces with `@Mock`. Never mock JPA repositories.
- Instantiate use cases manually via constructor.
- Naming: `&lt;UseCaseClass&gt;Test` (e.g., `CreateRaceUseCaseTest`).

```java
@ExtendWith(MockitoExtension.class)
class CreateRaceUseCaseTest {

    @Mock
    private RaceRepository raceRepository;

    private CreateRaceUseCase useCase;

    @BeforeEach
    void setUp() {
        useCase = new CreateRaceUseCase(raceRepository);
    }

    @Test
    void shouldCreateRaceWithPlannedStatus() {
        CreateRaceCommand command = new CreateRaceCommand("UTMB", "Chamonix");
        when(raceRepository.save(any(Race.class))).thenAnswer(inv -&gt; inv.getArgument(0));

        Race result = useCase.execute(command);

        assertNotNull(result.getId());
        assertEquals(Race.RaceStatus.PLANNED, result.getStatus());
        verify(raceRepository).save(any(Race.class));
    }
}
```

---

## Controller Tests (API Layer)

- Use `@WebMvcTest(&lt;Controller&gt;.class)` — loads only the web layer.
- Mock all use cases with `@MockBean`.
- Inject `MockMvc` with `@Autowired`.
- Do **not** use `@SpringBootTest` for controller tests.

```java
@WebMvcTest(RaceController.class)
class RaceControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private CreateRaceUseCase createRaceUseCase;

    @Test
    void shouldReturnCreatedStatusOnPost() throws Exception {
        when(createRaceUseCase.execute(any())).thenReturn(Race.builder().id("1").build());

        mockMvc.perform(post("/races")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"name\":\"UTMB\"}"))
                .andExpect(status().isCreated());
    }
}
```

---

## Architecture Tests

- Use ArchUnit in `HexagonalArchitectureTest` to enforce all layer boundaries.
- These tests live in `src/test/java/com/ultratrail/architecture/`.
- Do **not** use `@SpringBootTest` here.
- Run as standard JUnit 5 tests (`@AnalyzeClasses` + `@ArchTest`).

---

## General Rules

- Do **not** use `@SpringBootTest` for unit tests. It loads the full application context and is too heavy.
- Test class must be in the same package as the class under test.
- One test class per production class.
- Test method names use `should&lt;Behaviour&gt;When&lt;Condition&gt;` format.</code></pre>



<p>These files provide the necessary structure for our project, offering clear guidelines for our AI coding agents to work within.</p>



<h2 class="wp-block-heading">Extending the Application</h2>



<p>Now that we have our foundation in place, let&#8217;s put our ArchUnit tests and instruction files to the test. We&#8217;ve already established a solid structure with races, segments, and plans for each race. As we continue to grow our application, we&#8217;ll explore more features.</p>



<p>For instance, ultra races often take place far from home, spanning multiple days if the distance is significant, such as 100 miles or 160km. Planning accommodations as an athlete is crucial in these situations. Sometimes, the start and finish of a race can even be located in different areas.</p>



<p>To extend our application, I&#8217;ll send this prompt:</p>



<pre class="wp-block-code"><code class="">Extend the application by another feature. The feature is about accommodation. Each race needs accommodation possibilities that one can maintain in a CRUD manner. There could be a single accommodation for the duration of the race, and the days, before and after. But there could also be the case where an accommodation is taken before the race at the race start, and another one at the race finish location. Breakfast is an important option that should be made visible if available. Other than that the price and the distance to start or distance to the finish line are crucial. Another aspects is the number of guests. Sometimes you would like to travel with someone and need a double room. Sometimes a single room is enough and also cheaper.</code></pre>



<p>How does CoPilot work? After submitting the prompt, CoPilot springs into action, analyzing the request and generating a list of potential next steps. In this case, it identifies six TODOs that can be explored further.</p>



<p>The AI agent then uses its existing knowledge to study these patterns and find ways to integrate them seamlessly:</p>



<figure class="wp-block-image size-large mt-4"><img decoding="async" width="1024" height="492" src="https://ronnieschaniel.com/wp-content/uploads/2026/02/copilot_at_work_to_add_accommodations-1024x492.png" alt="" class="wp-image-2821" srcset="https://ronnieschaniel.com/wp-content/uploads/2026/02/copilot_at_work_to_add_accommodations-1024x492.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2026/02/copilot_at_work_to_add_accommodations-300x144.png 300w, https://ronnieschaniel.com/wp-content/uploads/2026/02/copilot_at_work_to_add_accommodations-768x369.png 768w, https://ronnieschaniel.com/wp-content/uploads/2026/02/copilot_at_work_to_add_accommodations.png 1494w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>CoPilot summarizes the changes by providing a report of all the passed tests, including the Hexagonal Architecture Test. This ensures that our codebase continues to adhere to the architecture we&#8217;ve established, ensuring stability and maintainability in the long run.</p>



<figure class="wp-block-image size-full mt-4"><img decoding="async" width="988" height="764" src="https://ronnieschaniel.com/wp-content/uploads/2026/02/after_adding_the_feature.png" alt="" class="wp-image-2819" srcset="https://ronnieschaniel.com/wp-content/uploads/2026/02/after_adding_the_feature.png 988w, https://ronnieschaniel.com/wp-content/uploads/2026/02/after_adding_the_feature-300x232.png 300w, https://ronnieschaniel.com/wp-content/uploads/2026/02/after_adding_the_feature-768x594.png 768w" sizes="(max-width: 988px) 100vw, 988px" /></figure>



<p>Finally, we can visually inspect our application to ensure everything is working as intended. For instance, let&#8217;s take a closer look at the Domain class:</p>



<pre class="wp-block-code"><code lang="java" class="language-java">@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Accommodation {
    private String id;
    private String raceId;
    private String name;
    private String location;
    private AccommodationType type;
    private LocalDate checkIn;
    private LocalDate checkOut;
    private Boolean breakfastIncluded;
    private BigDecimal pricePerNight;
    private BigDecimal distanceToStartKm;
    private BigDecimal distanceToFinishKm;
    private RoomType roomType;
    private Integer numberOfGuests;

    public enum AccommodationType {
        RACE_START, RACE_FINISH, FULL_STAY
    }

    public enum RoomType {
        SINGLE, DOUBLE
    }
}</code></pre>



<p>I like the categorisation in&nbsp;<code>RACE_START</code>,&nbsp;<code>RACE_FINISH</code>, and&nbsp;<code>FULL_STAY</code>. Additionally, the frontend has been extended (by a separate prompt) to display accommodation options, providing users with a more comprehensive view of their stay arrangements.</p>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="963" src="https://ronnieschaniel.com/wp-content/uploads/2026/02/race_detail_view_with_accommodation-1024x963.png" alt="" class="wp-image-2829" srcset="https://ronnieschaniel.com/wp-content/uploads/2026/02/race_detail_view_with_accommodation-1024x963.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2026/02/race_detail_view_with_accommodation-300x282.png 300w, https://ronnieschaniel.com/wp-content/uploads/2026/02/race_detail_view_with_accommodation-768x722.png 768w, https://ronnieschaniel.com/wp-content/uploads/2026/02/race_detail_view_with_accommodation-1536x1445.png 1536w, https://ronnieschaniel.com/wp-content/uploads/2026/02/race_detail_view_with_accommodation.png 1888w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>And there you have it! By leveraging ArchUnit tests and instruction files, we were able to extend our application with a new feature in just 10 minutes, without compromising its underlying structure or architecture. This approach has proven to be highly effective in promoting maintainability, scalability, and extensibility for future features.</p>



<h2 class="wp-block-heading">Summary</h2>



<p>As software engineers we ask ourselves how much we should vibe code? Additionally, we wonder what an application would look like if it were extended solely by AI over an extended period?</p>



<p>I believe that an application seriously deteriorates in quality over time if only extended by AI in a naive way without much guardrails. Understanding the architecture is more crucial than ever. But once that is defined well and put into mechanisms to verify, the engineer can focus on what really matters: the business logic and new features.</p>



<p>By striking a balance between human oversight and AI-assisted development, we can create applications that are both maintainable and scalable – a true win-win for software engineers.</p>



<p><sup><em>All my articles are written by myself. AI is used for some of the title images and in some cases to improve the wording and flow.</em></sup></p>
<p>The post <a href="https://ronnieschaniel.com/ai/hexagonal-architecture-in-spring-boot-projects-with-ai/">Hexagonal Architecture in Spring Boot Projects with AI</a> appeared first on <a href="https://ronnieschaniel.com">Ronnie Schaniel</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Race Report &#8211; Swiss Peaks 170</title>
		<link>https://ronnieschaniel.com/off-topic/race-report-swiss-peaks-170/</link>
		
		<dc:creator><![CDATA[ronnieschaniel@hey.com]]></dc:creator>
		<pubDate>Sun, 14 Sep 2025 18:51:53 +0000</pubDate>
				<category><![CDATA[Off Topic]]></category>
		<guid isPermaLink="false">https://ronnieschaniel.com/?p=2550</guid>

					<description><![CDATA[<p>Big climbs, alpine terrain and amazing views. My second attempt on the 100 miles.</p>
<p>The post <a href="https://ronnieschaniel.com/off-topic/race-report-swiss-peaks-170/">Race Report &#8211; Swiss Peaks 170</a> appeared first on <a href="https://ronnieschaniel.com">Ronnie Schaniel</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="768" src="https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_112906-1024x768.jpg" alt="" class="wp-image-2551" srcset="https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_112906-1024x768.jpg 1024w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_112906-300x225.jpg 300w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_112906-768x576.jpg 768w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_112906-1536x1152.jpg 1536w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_112906-2048x1536.jpg 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>Every other year, while browsing through available races, I’d inevitably find myself on&nbsp;<a href="http://swisspeaks.ch/" target="_blank" rel="noreferrer noopener">swisspeaks.ch</a>. The race takes place in Switzerland in September and looks absolutely stunning – everything seemed perfect on paper. But then reality hit: 168km, 10&#8217;990m of elevation gain, and 13&#8217;000m of elevation loss. The numbers alone sounded completely insane, and most of the course would wind through challenging alpine terrain. While I love a good challenge, I’ve always tried to stay realistic about my limits, so I’d quickly dismiss the idea of actually signing up.</p>



<figure data-wp-context="{&quot;imageId&quot;:&quot;69d45ec58e028&quot;}" data-wp-interactive="core/image" data-wp-key="69d45ec58e028" class="wp-block-image size-full mt-5 wp-lightbox-container"><img decoding="async" data-wp-class--hide="state.isContentHidden" data-wp-class--show="state.isContentVisible" data-wp-init="callbacks.setButtonStyles" data-wp-on--click="actions.showLightbox" data-wp-on--load="callbacks.setButtonStyles" data-wp-on-window--resize="callbacks.setButtonStyles" src="https://ronnieschaniel.com/wp-content/uploads/2025/09/Screenshot-2025-09-14-at-21.12.38.png" alt="" class="wp-image-2602"/><button
			class="lightbox-trigger"
			type="button"
			aria-haspopup="dialog"
			aria-label="Enlarge"
			data-wp-init="callbacks.initTriggerButton"
			data-wp-on--click="actions.showLightbox"
			data-wp-style--right="state.imageButtonRight"
			data-wp-style--top="state.imageButtonTop"
		>
			<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 12 12">
				<path fill="#fff" d="M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z" />
			</svg>
		</button><figcaption class="wp-element-caption">From <a href="https://swisspeaks.ch/170k/" target="_blank" rel="noreferrer noopener">swisspeaks.ch/170k</a></figcaption></figure>



<p>But 2025 was different. I had just completed my first 100-miler in 2024 with the <a href="https://ronnieschaniel.com/allgemein/race-report-utmb-nice-cote-dazur-100-miles/">Nice Côte d’Azur race</a> by UTMB, and in spring 2025, I achieved another major running goal by finishing the Zürich Marathon in under 3 hours. With these milestones behind me, I found myself free from concrete goals and genuinely ready to tackle something truly challenging. I was craving a race that would push me to my absolute limits – up until then, my races had gone relatively smoothly, and I’d never been in real danger of a DNF. So I did what any sensible runner would do: I signed up for the iconic Swiss Peaks 170.</p>



<h2 class="wp-block-heading">Training and Preparation</h2>



<p>At the end of June 2025, I ran the Lavaredo Ultra Trail 120km, followed by a well-deserved 10-day vacation in Italy. This meant I didn’t resume training until around July 10th for the early September Swiss Peaks start date – giving me exactly 8 weeks and 3 days to prepare. Since I already had a solid base from my training throughout the year, I decided not to focus on high volume but instead concentrate on race-specific preparation: tackling serious elevation gain and loss. I also mixed in some weekend hiking to round out the training.</p>



<p>As you can see, I peaked at 140km in my highest volume week, capped off the training block with a 42.3km long run, and then settled into a solid 2-week taper. For elevation, I maxed out at around 4&#8217;000m of gain in one particular week, while most others hovered around the 3&#8217;000m mark.</p>



<div class="wp-block-columns mt-5 is-layout-flex wp-container-core-columns-is-layout-9d6595d7 wp-block-columns-is-layout-flex">
<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow" style="flex-basis:100%">
<div class="wp-block-group is-layout-grid wp-container-core-group-is-layout-148d70a5 wp-block-group-is-layout-grid">
<figure class="wp-block-image size-large"><img decoding="async" width="669" height="1024" src="https://ronnieschaniel.com/wp-content/uploads/2025/09/Screenshot_20250910_181644_Strava-669x1024.jpg" alt="" class="wp-image-2553" srcset="https://ronnieschaniel.com/wp-content/uploads/2025/09/Screenshot_20250910_181644_Strava-669x1024.jpg 669w, https://ronnieschaniel.com/wp-content/uploads/2025/09/Screenshot_20250910_181644_Strava-196x300.jpg 196w, https://ronnieschaniel.com/wp-content/uploads/2025/09/Screenshot_20250910_181644_Strava-768x1176.jpg 768w, https://ronnieschaniel.com/wp-content/uploads/2025/09/Screenshot_20250910_181644_Strava-1003x1536.jpg 1003w, https://ronnieschaniel.com/wp-content/uploads/2025/09/Screenshot_20250910_181644_Strava.jpg 1079w" sizes="(max-width: 669px) 100vw, 669px" /></figure>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="746" src="https://ronnieschaniel.com/wp-content/uploads/2025/09/Screenshot_20250910_181719_Strava-1024x746.jpg" alt="" class="wp-image-2554" srcset="https://ronnieschaniel.com/wp-content/uploads/2025/09/Screenshot_20250910_181719_Strava-1024x746.jpg 1024w, https://ronnieschaniel.com/wp-content/uploads/2025/09/Screenshot_20250910_181719_Strava-300x219.jpg 300w, https://ronnieschaniel.com/wp-content/uploads/2025/09/Screenshot_20250910_181719_Strava-768x559.jpg 768w, https://ronnieschaniel.com/wp-content/uploads/2025/09/Screenshot_20250910_181719_Strava.jpg 1079w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>
</div>
</div>
</div>



<p>Planning for an ultra of this magnitude – with its variable terrain and unpredictable weather – is no simple task. As always, I created three different race plans: A, B, and C, targeting finishing times of 32, 36, and 40 hours respectively.</p>



<h2 class="wp-block-heading">Before the Race</h2>



<p>The race starts in a pretty remote location: at Europe’s largest dam, <a href="https://fr.wikipedia.org/wiki/Barrage_de_la_Grande-Dixence" target="_blank" rel="noreferrer noopener">Barrage de la Grande Dixence</a>, sitting at 2’364m above sea level. You have two shuttle options from Bouveret at the Lake Geneva – either at 17:00 the day before the race or at 5:30 on race morning to make the 10:00 start. I opted for the earlier shuttle and booked a room at the hotel near the dam. Pro tip: book early, as accommodation fills up fast.</p>



<p>After picking up my bib at 15:00 in Bouveret, I walked the 20 minutes to the shuttle departure point and grabbed some supplies at the local supermarket. The drive up to Dixence took longer than expected – we started late and made a few stops along the way. I finally checked in around 19:15, just in time to catch the tail end of the 19:00 dinner service. Fortunately, there was still plenty of space and the food was surprisingly good.</p>



<figure class="wp-block-image size-large is-resized mt-5"><img decoding="async" width="768" height="1024" src="https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_083715-768x1024.jpg" alt="" class="wp-image-2577" style="width:604px;height:auto" srcset="https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_083715-768x1024.jpg 768w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_083715-225x300.jpg 225w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_083715-1152x1536.jpg 1152w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_083715-1536x2048.jpg 1536w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_083715-scaled.jpg 1920w" sizes="(max-width: 768px) 100vw, 768px" /></figure>



<p>During dinner, I chatted with some other runners. One of them had already tackled both the 380km and 170km versions of Swiss Peaks – quite the veteran! Eventually, conversation turned to rumors that the race might be temporarily halted tomorrow due to thunderstorms forecasted for early evening. Around 20:30, we received official confirmation via email and SMS: the race would be neutralized at Lourtier (the 2nd aid station at 20km), with a mandatory waiting period from 17:30 to 22:00, followed by a mass restart. Essentially, it would run like a stage race – your arrival time in Lourtier would count, then the clock would reset at 22:00 for the mass start. Interesting approach, and honestly, it didn’t bother me much. Safety comes first.<br>Since I’d booked a room with a private bathroom, I had my peace and quiet for a good night’s sleep. The breakfast was solid too, and I made sure to get some coffee in me to properly wake up for the day ahead.</p>



<h2 class="wp-block-heading">The Race</h2>



<p>There are two ways to get to the starting line on the dam: you can take the cable car, or walk up. It&#8217;s a 1km climb with 230m of elevation gain, and I decided to go for the walk! I dropped my baggage, plus the drop bag, at the hotel where they were loaded onto a truck. My main baggage would be transported back to Bouveret – hopefully where I&#8217;ll be finishing! The drop bag will be available at Lourtier during the break, and then at the two official life bases along the course.</p>



<h3 class="wp-block-heading">Start to Stop</h3>



<p>It had started to rain on the dam, but it stopped a few minutes before the start. The organizers then explained again the reasons for the stop in Lourtier. They <em>could</em> have merged us with the 100km runners, but they wanted to give us the full 100-mile experience – and that’s what we signed up for! Everything has been well organized and clearly explained so far.</p>



<figure class="wp-block-image size-large mt-5"><img decoding="async" width="768" height="1024" src="https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_101322-768x1024.jpg" alt="" class="wp-image-2560" srcset="https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_101322-768x1024.jpg 768w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_101322-225x300.jpg 225w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_101322-1152x1536.jpg 1152w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_101322-1536x2048.jpg 1536w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_101322-scaled.jpg 1920w" sizes="(max-width: 768px) 100vw, 768px" /></figure>



<p>The start was delayed by 30 minutes for various reasons. At 10:30 we finally set off, beginning our first climb from 2364m up to the highest point of the course at 2985m. I kept my poles attached to my running bag for the first few minutes to navigate the crowd. It’s good to say the start, with around 230 people, was surprisingly pleasant – a far cry from the 1600 at Lavaredo! After leaving the dam, the climb began immediately, which quickly stretched the runners out. I was happy with my position and aimed to ease into the race, maintaining a pace of around 10:00 min/km. I adjusted my tactics slightly, deciding to push a bit harder over the first 20km. We’d have plenty of time to recover during the break afterwards, so it made sense to bank some time here.</p>



<figure class="wp-block-image size-large mt-5"><img decoding="async" width="1024" height="683" src="https://ronnieschaniel.com/wp-content/uploads/2025/09/d92cbd2d-c2fe-4e2f-869f-522ca6027ccf-1024x683.jpg" alt="" class="wp-image-2591" srcset="https://ronnieschaniel.com/wp-content/uploads/2025/09/d92cbd2d-c2fe-4e2f-869f-522ca6027ccf-1024x683.jpg 1024w, https://ronnieschaniel.com/wp-content/uploads/2025/09/d92cbd2d-c2fe-4e2f-869f-522ca6027ccf-300x200.jpg 300w, https://ronnieschaniel.com/wp-content/uploads/2025/09/d92cbd2d-c2fe-4e2f-869f-522ca6027ccf-768x512.jpg 768w, https://ronnieschaniel.com/wp-content/uploads/2025/09/d92cbd2d-c2fe-4e2f-869f-522ca6027ccf-1536x1024.jpg 1536w, https://ronnieschaniel.com/wp-content/uploads/2025/09/d92cbd2d-c2fe-4e2f-869f-522ca6027ccf-2048x1365.jpg 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>The climb was rocky, and eventually we reached a plateau. Even there, the terrain was quite technical, but I stayed focused and managed to stay on my feet – a definite improvement over the three falls I took in the first hour of my last ultra! Shortly after the highest point, we reached a water station. There were  two volunteers handing out water in bottles. I refilled one flask before continuing on towards the Grand Desert. There were some rocky sections requiring climbing, but early in the race, it didn’t bother me too much.</p>



<figure class="wp-block-image size-large mt-5"><img decoding="async" width="1024" height="768" src="https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_115608-1024x768.jpg" alt="" class="wp-image-2562" srcset="https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_115608-1024x768.jpg 1024w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_115608-300x225.jpg 300w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_115608-768x576.jpg 768w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_115608-1536x1152.jpg 1536w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_115608-2048x1536.jpg 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>After a few more minutes, I reached a single-track trail that was a little less technical, allowing us to run for a bit before the biggest downhill of the race. That downhill was tough – 1600m of negative elevation over 6.5km in the main section. One thing I’d promised myself for this race was to push harder on the downhills. Somehow, I managed it without my quads screaming too much, and eventually reached a paved road where the terrain began to flatten out.</p>



<p>I arrived in Lourtier around 13:45 for the big break. I was ranked 34th at that time, which allowed me to find a good spot in the gym. I grabbed a mat near the wall and took advantage of a power outlet to charge my phone and watch. I couldn’t really sleep, but I managed to relax and recover from the long downhill. Lourtier is located in the Val de Bagnes, and anyone familiar with Switzerland and Valais knows the region is famous for Raclette. It might not be the best fuel for running, but I still had around 5-6 hours before the race restarted, so I enjoyed a small portion of the famous cheese dish. A big thanks to the fantastic volunteers!</p>



<div class="wp-block-columns mt-5 is-layout-flex wp-container-core-columns-is-layout-9d6595d7 wp-block-columns-is-layout-flex">
<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<figure class="wp-block-image size-full is-resized"><img decoding="async" src="https://ronnieschaniel.com/wp-content/uploads/2025/09/lourtier_break-2.png" alt="" class="wp-image-2575" style="width:368px;height:auto"/></figure>
</div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<figure class="wp-block-image size-large is-resized"><img decoding="async" width="768" height="1024" src="https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_164006-768x1024.jpg" alt="" class="wp-image-2564" style="width:341px;height:auto" srcset="https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_164006-768x1024.jpg 768w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_164006-225x300.jpg 225w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_164006-1152x1536.jpg 1152w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_164006-1536x2048.jpg 1536w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250904_164006-scaled.jpg 1920w" sizes="(max-width: 768px) 100vw, 768px" /></figure>
</div>
</div>



<p>Eventually, I moved from the gym to another room to sit at a table. I started chatting with fellow runners from Zürich, Ticino, and Hong Kong. It was a more pleasant atmosphere than at UTMB races – maybe partly because we all needed a break! A thunderstorm was passing by, and we were all glad not to be high up in the mountains at that time. Around 19:00 we were called back to the gym, where they explained that the race would continue with a mass start at 21:00. The cold kit was activated, meaning we had to carry it. Luckily, I found enough space in my 12l running vest, so I packed my waterproof overpants and an extra warm down jacket. Some runners started putting on the overpants, but there was a 1000m climb over the next 5km. It didn’t make sense to me – I’d overheat, even in the rain.</p>



<h3 class="wp-block-heading">Getting into the first night</h3>



<p>After a seven-hour break – at least for me – we headed out into the night for a second start to the race. There was still a 150km ultra to go after this 20km &#8220;sky race&#8221;. The climb was steep, as expected, so I was glad to reach the aid station after just a few kilometers. After that, the trail became more runnable – still a single track with ups and downs, but no major climbs or descents, except for the final section to Mont Brûlé. The descent from Mont Brûlé was also fairly runnable, and much of it took place on forest roads. That’s when my stomach started to rebel, and I was relieved to take a small break at the next aid station in Prassurny to recover.</p>



<p>What followed was the climb to Fenêtre d&#8217;Arpette. 1500m up over 10km, and the terrain gradually became quite rocky. It got steeper and steeper, and we were soon climbing from rock to rock. The hardest part wasn’t the physical exertion, but the darkness. I couldn’t see the top, and it was difficult to gauge how much further we had to go. I caught glimpses of headlamps ahead, which eventually disappeared – they must have reached the summit. Eventually, I did too, feeling a sense of accomplishment despite the fatigue.</p>



<p>The descent to Col de la Forclaz brought a welcome end to the night, and I was able to turn off my headlamp. A longer, flatter section followed alongside the famous historic Bisse (water transport system). This is were I was faster again and overtook some runners. Reaching the aid station required some careful navigation through muddy terrain for the last 100m – I was relieved to stay upright. I grabbed some bread with honey for breakfast and headed out for the next climb a few minutes later, wondering what challenges lay ahead.</p>



<h3 class="wp-block-heading">Green with light rain</h3>



<p>For the next section, I found myself running alone for at least an hour. It wasn’t a blistering pace, but I wasn’t being overtaken either – a solid, steady effort. On a flat section, I had my first fall of the race. Luckily, it wasn’t serious, and I was able to get back on my feet and refocus.</p>



<figure class="wp-block-image size-large is-resized mt-5"><img decoding="async" width="768" height="1024" src="https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_092258-768x1024.jpg" alt="" class="wp-image-2565" style="width:543px;height:auto" srcset="https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_092258-768x1024.jpg 768w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_092258-225x300.jpg 225w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_092258-1152x1536.jpg 1152w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_092258-1536x2048.jpg 1536w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_092258-scaled.jpg 1920w" sizes="(max-width: 768px) 100vw, 768px" /></figure>



<p>I reached the first life base in Salvan around lunchtime, and thankfully, the sun had come out and the rain had stopped for a while. I took a longer break there to eat some pasta. We were allowed to continue without the cold kit after this life base, so I removed it from my running pack, which made my vest a bit lighter. That was good timing, as we were immediately faced with a 1600m climb to Col de Susanfe, reaching 2493m, with an aid station around 1900m.</p>



<figure class="wp-block-image size-large is-resized mt-5"><img decoding="async" width="768" height="1024" src="https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_112635-768x1024.jpg" alt="" class="wp-image-2566" style="width:549px;height:auto" srcset="https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_112635-768x1024.jpg 768w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_112635-225x300.jpg 225w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_112635-1152x1536.jpg 1152w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_112635-1536x2048.jpg 1536w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_112635-scaled.jpg 1920w" sizes="(max-width: 768px) 100vw, 768px" /></figure>



<p>A large part of the climb consisted of stairs – a surprisingly welcome change. It offered a break from the usual climbing and allowed different muscle groups to work. I switched both poles into one hand and used the other to steady myself on the rail. At times, it felt almost canyon-like, with water rushing underneath. Eventually, the stairs gave way to a double track, and a few minutes later, I reached the Auberge de Salanfe aid station – a large building with a small aid station tucked away on the ground floor.</p>



<figure class="wp-block-image size-large mt-5"><img decoding="async" width="1024" height="768" src="https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_130712-1024x768.jpg" alt="" class="wp-image-2567" srcset="https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_130712-1024x768.jpg 1024w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_130712-300x225.jpg 300w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_130712-768x576.jpg 768w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_130712-1536x1152.jpg 1536w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_130712-2048x1536.jpg 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>I left the aid station a few minutes later and gratefully enjoyed a flatter section. The rain had definitely stopped, and I was able to shed layers, reducing to just a t-shirt – a welcome relief after the earlier, wetter stages of the race.</p>



<figure class="wp-block-image size-large mt-5"><img decoding="async" width="768" height="1024" src="https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_140253-768x1024.jpg" alt="" class="wp-image-2568" srcset="https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_140253-768x1024.jpg 768w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_140253-225x300.jpg 225w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_140253-1152x1536.jpg 1152w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_140253-1536x2048.jpg 1536w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_140253-scaled.jpg 1920w" sizes="(max-width: 768px) 100vw, 768px" /></figure>



<p>The climb that followed was steep, but thankfully not as demanding or technical as Fenêtre d&#8217;Arpette. As usual on climbs, I felt strong and was able to keep pace with one of the runners from Ticino I’d chatted with earlier. Reaching the top, we encountered a small, windswept ridge, and I hurried to move on.</p>



<figure class="wp-block-image size-large mt-5"><img decoding="async" width="768" height="1024" src="https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_140304-768x1024.jpg" alt="" class="wp-image-2569" srcset="https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_140304-768x1024.jpg 768w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_140304-225x300.jpg 225w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_140304-1152x1536.jpg 1152w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_140304-1536x2048.jpg 1536w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_140304-scaled.jpg 1920w" sizes="(max-width: 768px) 100vw, 768px" /></figure>



<h3 class="wp-block-heading">Struggle</h3>



<p>The downhill that followed proved difficult again. I fell once, hitting my elbow, and while it wasn&#8217;t serious, it definitely rattled my concentration. After the downhill, I slogged through some muddy terrain before finally reaching the Barme aid station at 102km. I took a fifteen-minute break, but I was shivering a bit and my nutrition wasn&#8217;t sitting well the last hour. I sent a few messages to those supporting me, then planned to push on to Morgins, the second life base 20km away, hoping for a longer break to replenish my energy. Easier said than done. As you can see in the pictures below, the terrain wasn’t letting up. The slowest section was probably the downhill secured by chains.</p>



<figure class="wp-block-image size-large mt-5"><img decoding="async" width="768" height="1024" src="https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_151332-768x1024.jpg" alt="" class="wp-image-2570" srcset="https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_151332-768x1024.jpg 768w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_151332-225x300.jpg 225w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_151332-1152x1536.jpg 1152w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_151332-1536x2048.jpg 1536w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250905_151332-scaled.jpg 1920w" sizes="(max-width: 768px) 100vw, 768px" /></figure>



<p>Reaching Morgins during daylight was becoming increasingly unrealistic the longer this section took. I climbed again after Barme, had a quick phone call with my girlfriend, and met a French runner along the way. There was a flat section for a while, but I didn’t feel much like running. So, we alternated between running and power hiking. We missed the water station at kilometer 112, but it didn’t matter too much – I still had enough water. Soon after, the downhill began, and with it, the sun set, forcing me to pull out my headlamp for the second night. It flashed three times shortly after, then again a few minutes later. Better to change the battery. Another break that further delayed my arrival at Morgins. But at that point, I&#8217;d managed to eat a bit better, and a flat stretch down in the valley (at 1400m) stood between me and the next life base.<br>I promised myself I’d run this section. It would have been beautiful in daylight – the path followed a river, and we crossed small bridges seven or eight times. Around 21:00, I finally reached the life base in Morgins, after what felt like an eternity. My watch showed over 122km, which had made it harder, as I was expecting the base to appear shortly after the 122nd kilometer. Anyway, now it was time for a break. I ate rice with vegetables and tried to calm down, then planned to sleep for 40 minutes. I took my sleeping bag from the drop bag and headed upstairs with my phone and the alarm set. In one of the rooms, I found an empty bed. Only one other runner was in the same room – probably one of the 380 or 700km Swiss Peaks competitors. He was sleeping. I didn&#8217;t want to disturb him and quietly settled into my sleeping bag. Simply lying down was helping. I don&#8217;t know how much deep sleep I actually got – maybe ten minutes – but lying flat and releasing the pressure on my legs for forty minutes felt amazing! I got up and went downstairs for a coffee.</p>



<p>I spent some time in the room downstairs, drinking and eating. As is typical at Swiss Peaks, they offered local food from Valais, and the volunteers were incredibly helpful. The runner from Ticino I’d seen earlier said goodbye and headed out, just as a German runner I met during the long break in Lourtier arrived. He looked tired, but he&#8217;d already completed the 380km once, so he was a very experienced runner and I was confident he’d make it. I pointed him towards the sleeping places and slowly began getting ready to dash out into the night around 23:00. Isn’t it crazy to leave a warm, cozy place and start running so late in the evening? It was, but I wasn’t struggling with sleep deprivation at this stage. The coffee helped, and I also took some gels with caffeine. The key was likely that I’d had very good sleep the nights before the race.</p>



<p>And uphill to Conche it was. I was hoping the rest of the race would be easier – only 46km with 2460m of ascent and 3430m of descent remaining, and we wouldn’t climb above 2000 meters again. Later, I’d learn that didn’t quite pan out. Anyway, I felt much better, and the distances between aid stations were getting shorter in this later stage. After only 6km, I reached Conche and headed out quickly after a few minutes. Another 300m climb brought us onto a ridge, but it was difficult to run – spiked with rocks. We ran through the area of a ski resort, so at least part of it was runnable after the ridge. A little up and down, and I reached the next aid station, titled Chalet de Blancsex – though it wasn’t a chalet, just a small white tent. It was 3:00 AM and cold. I took a bouillon in my cup but started shivering heavily. The volunteers were kind and brought me closer to their small oven and gave me a warm blanket. It helped a bit, but I knew I had to get out and moving. So, after a few minutes, I grabbed my poles again and started the next climb. I was shivering and breathing heavily at first, trying to raise my body temperature, but after a few minutes, I relaxed again and warmed up. I’d put on my race leggings at the last aid station, but they didn’t allow me to move comfortably, so I took them off again. Another few minutes lost for a silly decision. Taney was the next aid station, and I didn’t want to linger there, not moving. There was one last 400m uphill – the final significant climb. I really enjoyed the landscape now – it was softer, and we’d be running on a double track.</p>



<h3 class="wp-block-heading">The sun comes out</h3>



<p>The section at the top was flat and a few minutes after it I started the downhill, and the sun rose. I’d successfully navigated the second night – no falls, no major issues apart from the cold. What really lifted my mood, though, was the sight of Lac Léman. The finish line was located at that lake, and it was now in sight – not the finish line itself, of course, but at least the lake. And now I was certain – I could make it. I could really finish this incredibly tough 100-mile race. Before the race, I’d told myself that a DNF was certainly a possibility. I’d never DNFed before in any of the six ultras I’d run to that point, but Swiss Peaks was a level above everything else I’d done so far in terms of difficulty – more climbs, distance, downhills, technical terrain, and alpine conditions than anything before. But I was also as fit as I’d ever been.</p>



<figure class="wp-block-image size-large mt-5"><img decoding="async" width="768" height="1024" src="https://ronnieschaniel.com/wp-content/uploads/2025/09/20250906_064736-768x1024.jpg" alt="" class="wp-image-2557" srcset="https://ronnieschaniel.com/wp-content/uploads/2025/09/20250906_064736-768x1024.jpg 768w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250906_064736-225x300.jpg 225w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250906_064736-1152x1536.jpg 1152w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250906_064736-1536x2048.jpg 1536w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250906_064736-scaled.jpg 1920w" sizes="(max-width: 768px) 100vw, 768px" /></figure>



<p>What also helped at this stage was that the people supporting me were awake again. I received more encouraging messages – they were delighted that I’d continued after Morgins and was progressing well through the second night. During the downhill, I heard something rustling in the grass. I turned to my right and saw an ibex! I was very happy to see one – I’d hoped to spot one early in the race.</p>



<figure class="wp-block-image size-large mt-5"><img decoding="async" width="768" height="1024" src="https://ronnieschaniel.com/wp-content/uploads/2025/09/20250906_070823-768x1024.jpg" alt="" class="wp-image-2558" srcset="https://ronnieschaniel.com/wp-content/uploads/2025/09/20250906_070823-768x1024.jpg 768w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250906_070823-225x300.jpg 225w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250906_070823-1152x1536.jpg 1152w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250906_070823-1536x2048.jpg 1536w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250906_070823-scaled.jpg 1920w" sizes="(max-width: 768px) 100vw, 768px" /></figure>



<p>It was only the second time I’d seen a wild one. It’s the heraldic animal of my home canton. However, I didn’t want to disturb him too much and continued my downhill. The next section was then really frustrating – a single trail through the forest that wasn’t easy, full of stones, roots, and wet patches. At that stage, it annoyed me. Come on, I’d already shown I could run in tough terrain – why did it need to continue like this? At the last aid station, I took a five-minute break and refilled for the final 11km.</p>



<figure class="wp-block-image size-large mt-5"><img decoding="async" width="768" height="1024" src="https://ronnieschaniel.com/wp-content/uploads/2025/09/20250906_082529-768x1024.jpg" alt="" class="wp-image-2559" srcset="https://ronnieschaniel.com/wp-content/uploads/2025/09/20250906_082529-768x1024.jpg 768w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250906_082529-225x300.jpg 225w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250906_082529-1152x1536.jpg 1152w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250906_082529-1536x2048.jpg 1536w, https://ronnieschaniel.com/wp-content/uploads/2025/09/20250906_082529-scaled.jpg 1920w" sizes="(max-width: 768px) 100vw, 768px" /></figure>



<p>At least those last kilometers had some easier sections – a wide downhill in the forest that was quite runnable. Well, running at that stage meant a pace of 7:00 min/km, and I was motivated to finish in under 40 hours. I ran into Bouveret, where the finish line was located and where I’d picked up my bib three days before. There wasn’t a direct path to the finish line, though – first, a big turn to the left. Finally, I reached the train station, from which it was only one kilometer to the finish line, and I started to speed up. People were cheering, and I had a clear sight of the finish line now. One last paved path straight towards the lake, and I crossed it. I’d really made it – 173km with 11,055m of elevation gain and 13,012m of elevation loss in 39h 48m.</p>



<p>I finished 35th and was quite happy with it. Although the main goal was simply to finish, I was glad to have done so in a reasonable time.</p>



<figure class="wp-block-image size-full is-resized mt-5"><img decoding="async" width="878" height="516" src="https://ronnieschaniel.com/wp-content/uploads/2025/09/Screenshot-2025-09-14-at-20.38.03.png" alt="" class="wp-image-2586" style="width:486px;height:auto" srcset="https://ronnieschaniel.com/wp-content/uploads/2025/09/Screenshot-2025-09-14-at-20.38.03.png 878w, https://ronnieschaniel.com/wp-content/uploads/2025/09/Screenshot-2025-09-14-at-20.38.03-300x176.png 300w, https://ronnieschaniel.com/wp-content/uploads/2025/09/Screenshot-2025-09-14-at-20.38.03-768x451.png 768w" sizes="(max-width: 878px) 100vw, 878px" /></figure>



<p>After crossing the finish line, not much happened. One person at the table with drinks and food cheered for me, but there wasn&#8217;t much activity around. It didn&#8217;t matter, though, because I ran this race for myself and the people who supported me from home. I took a slice of cheese and a water and walked further. Then they explained that they were having issues with the production of our medals and would send them home. I did receive a running vest as a finish gift, which was nice. I made a few calls and quickly congratulated one of the French runners I’d met earlier when we missed the water station.</p>



<h2 class="wp-block-heading">After the Race</h2>



<p>I collected my baggage and drop bag and headed for a shower and change of clothes. The train journey home took almost four hours. I slept a bit during the ride, setting my alarm clock to make sure I didn’t miss any connections. When I reached home, I quickly talked with my girlfriend before going to bed for a two-hour nap before dinner.</p>



<p>My physical recovery after the race was quick. Already the next day, I went for a short walk in the forest to get my legs moving, and it felt fine. Mentally, I was a bit off, perhaps due to the sleep deprivation or just the sheer effort. However, that improved eventually. I didn’t run the week after the race, but we went for a two-day hike (38km in total) seven days after my finish. I was skeptical at first, but the hike wasn’t a problem at all. I enjoyed some good wine and food – a refreshing change, as I’d been very focused on nutrition in the weeks leading up to the race. Normal life was back, time to relax and see friends more often. No more races until the end of the year, probably!</p>



<p>A huge thank you goes to everyone who supported me during the race. Without you, I might not have finished it.</p>
<p>The post <a href="https://ronnieschaniel.com/off-topic/race-report-swiss-peaks-170/">Race Report &#8211; Swiss Peaks 170</a> appeared first on <a href="https://ronnieschaniel.com">Ronnie Schaniel</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>The Cost and Time Model of AI-Assisted Software Engineering</title>
		<link>https://ronnieschaniel.com/ai/the-cost-and-time-model-of-ai-assisted-software-engineering/</link>
		
		<dc:creator><![CDATA[ronnieschaniel@hey.com]]></dc:creator>
		<pubDate>Sun, 08 Feb 2026 13:57:32 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<guid isPermaLink="false">https://ronnieschaniel.com/?p=2761</guid>

					<description><![CDATA[<p>The impact of coding assistants on the SDLC.</p>
<p>The post <a href="https://ronnieschaniel.com/ai/the-cost-and-time-model-of-ai-assisted-software-engineering/">The Cost and Time Model of AI-Assisted Software Engineering</a> appeared first on <a href="https://ronnieschaniel.com">Ronnie Schaniel</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="572" src="https://ronnieschaniel.com/wp-content/uploads/2026/01/AI_aided_software_engineering_costs_gemini-1024x572.png" alt="" class="wp-image-2763" srcset="https://ronnieschaniel.com/wp-content/uploads/2026/01/AI_aided_software_engineering_costs_gemini-1024x572.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2026/01/AI_aided_software_engineering_costs_gemini-300x167.png 300w, https://ronnieschaniel.com/wp-content/uploads/2026/01/AI_aided_software_engineering_costs_gemini-768x429.png 768w, https://ronnieschaniel.com/wp-content/uploads/2026/01/AI_aided_software_engineering_costs_gemini.png 1376w" sizes="(max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Generated by Gemini.</figcaption></figure>



<p>AI coding assistants emerged in spring 2023. Since then, those who aren&#8217;t familiar with software engineering have predicted that the role of the software engineer would become obsolete. Three years on, we&#8217;re still here. While our daily work has undergone changes, so too must our calculations of costs and understanding of time.</p>



<h2 class="wp-block-heading">The Cost and Time View before AI Coding Assistants</h2>



<p>Let us first consider the traditional approach. When faced with a task, one, two, or many engineers would be assigned to implement it, depending on their Agile methodology. The design phase, whether lengthy or brief, would precede implementation. The engineer(s) involved would have an in-depth understanding of all details. Next, the development process would unfold, including reviewing written code, manually or automatically testing on integration environments, and deployment and release. In the event of detected issues, they would be addressed. Crucially, knowledge was embedded within the engineers participating in each phase, making subsequent changes more manageable.</p>



<p>Typically, developers focused on a single task at a time, aiming to minimize context switches due to their inherent cost. Team velocity was calculated by dividing delivered story points by capacity.</p>



<p>This is how I see software engineering without AI support:</p>



<figure class="wp-block-image size-full"><img decoding="async" width="2560" height="243" src="https://ronnieschaniel.com/wp-content/uploads/2026/02/non_ai_software_engineering-scaled.png" alt="" class="wp-image-2767" srcset="https://ronnieschaniel.com/wp-content/uploads/2026/02/non_ai_software_engineering-scaled.png 2560w, https://ronnieschaniel.com/wp-content/uploads/2026/02/non_ai_software_engineering-300x28.png 300w, https://ronnieschaniel.com/wp-content/uploads/2026/02/non_ai_software_engineering-1024x97.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2026/02/non_ai_software_engineering-768x73.png 768w, https://ronnieschaniel.com/wp-content/uploads/2026/02/non_ai_software_engineering-1536x146.png 1536w, https://ronnieschaniel.com/wp-content/uploads/2026/02/non_ai_software_engineering-2048x194.png 2048w" sizes="(max-width: 2560px) 100vw, 2560px" /></figure>



<ul class="wp-block-list">
<li>In Agile environments, the design phase is relatively brief. You might consider how a new user story fits into the overall system, but you wouldn&#8217;t typically document every detail of your planned implementation.</li>



<li>The majority of work is invested in the implementation and maintenance phases. While some may argue for an even longer maintenance period.</li>



<li>The review phase, which includes merge request reviews, relies heavily on the quality and clarity of the implementation. It tends to be shorter than the implementation phase itself.</li>



<li>Testing encompasses both automated and manual acceptance testing, with the majority of testing ideally taking place during the implementation phase. The quality of the implementation has a significant impact on this process. In general, writing automated acceptance tests at the UI level requires some time.</li>
</ul>



<p>From a cost or effort perspective, we observe the following:</p>



<ul class="wp-block-list">
<li>Developers typically focus on one task at a time, dedicating their attention to that specific project.</li>



<li>On the cost side, there is the developer&#8217;s salary, as well as expenses for traditional tools and licenses.</li>
</ul>



<p>Now, let us examine how this has evolved with the emergence of coding assistants.</p>



<h2 class="wp-block-heading">The Current State of AI-Supported Software Engineering</h2>



<p>We are having a look at the situation when AI coding assistants are used by the developers. The following picture illustrates the shifted distribution of phases:</p>



<figure class="wp-block-image size-full"><img decoding="async" width="2560" height="242" src="https://ronnieschaniel.com/wp-content/uploads/2026/02/ai_software_engineering-scaled.png" alt="" class="wp-image-2769" srcset="https://ronnieschaniel.com/wp-content/uploads/2026/02/ai_software_engineering-scaled.png 2560w, https://ronnieschaniel.com/wp-content/uploads/2026/02/ai_software_engineering-300x28.png 300w, https://ronnieschaniel.com/wp-content/uploads/2026/02/ai_software_engineering-1024x97.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2026/02/ai_software_engineering-768x73.png 768w, https://ronnieschaniel.com/wp-content/uploads/2026/02/ai_software_engineering-1536x145.png 1536w, https://ronnieschaniel.com/wp-content/uploads/2026/02/ai_software_engineering-2048x193.png 2048w" sizes="(max-width: 2560px) 100vw, 2560px" /></figure>



<ul class="wp-block-list">
<li>In this new scenario, the design phase typically becomes longer. This is because you need to provide more detailed specifications upfront for the AI agent to effectively implement the requirements.</li>



<li>The most significant change occurs in the Implementation phase, which tends to be shortened. Writing code is relatively easy for coding assistants to handle.</li>



<li>On the other hand, the review process becomes even more crucial and time-consuming. First, you must read the description of what the AI assistant did, followed by a review of the actual code itself.</li>



<li>The testing phase itself might become slightly shorter, as automated acceptance tests can also be generated by AI. On the other hand you need to ensure that the right thing was implemented by the AI and acceptance criteria are met.</li>



<li>Finally, I would argue that the maintenance phase is expanded relatively seen. As you haven&#8217;t been intimately involved in every detail yourself, finding bugs and understanding existing code can be more challenging. Moreover, AI often tends to create verbose code, making implementation more complicated than it needs to be – a situation that leads to more and harder-to-maintain code. For sure, also AI can be used to debug which again helps to keep the effort lower.</li>
</ul>



<p>From a cost or effort perspective, we observe the following:</p>



<ul class="wp-block-list">
<li>Developers can now work on multiple tasks at a time, especially when utilizing the agentic mode of AI coding assistants. However, this comes with increased context switching, which has its own set of drawbacks.</li>



<li>On the cost side, there is the developer&#8217;s salary, as well as traditional tools and licenses. Furthermore, expenses arise from the fee associated with using the coding assistant.</li>
</ul>



<h2 class="wp-block-heading">Consequences for Software Engineering</h2>



<h3 class="wp-block-heading">The Design Phase: More Important than Ever</h3>



<p>In this new world of AI-assisted software engineering, the design phase becomes even more critical. Implicit knowledge that was previously shared through informal channels or coffee breaks now needs to be documented and provided as input for AI.</p>



<h3 class="wp-block-heading">Implementation Phase</h3>



<p>Developers are typically fast typists, but AI can generate code even faster. This results in a shortened implementation phase. Moreover, multiple agents can run in parallel, allowing developers to perform other tasks simultaneously. A developer can have a coding agent running in the background while checking emails, for instance.</p>



<p>However, this increased productivity also introduces the danger of complacency. Developers might become too reliant on AI-generated code and neglect the importance of reviewing and maintaining it. As such, the review phase assumes greater significance, requiring more time and expertise from engineers.</p>



<h3 class="wp-block-heading">Review</h3>



<p>When so much code is produced, review can become a bottleneck (also suggested in the <a href="https://dora.dev/research/2025/dora-report/">2025 DORA report</a>). And if it is not done properly, it can lead to quality issues. What makes reviewing even more challenging, is that the code which was produced is not necessarily understandable by juniors or even some seniors. While being careful is also here more important than ever, using AI in the review process itself can also help.</p>



<h3 class="wp-block-heading">Testing</h3>



<p>Testing remains a vital aspect of software engineering. While some automated acceptance tests can be generated by AI, manual testing is still essential for ensuring the quality of the final product.</p>



<h3 class="wp-block-heading">Maintenance Phase</h3>



<p>Maintaining code is all about fixing problems, monitoring performance, and extending functionality. In this new world, I believe this phase requires more effort relative to the original process. Developers are less familiar with the generated code, which can lead to increased mental stress when switching between multiple instances of AI-generated code.</p>



<h3 class="wp-block-heading">Cost Considerations</h3>



<p>License and tool costs have always been a consideration in software engineering. The rise of AI-assisted coding tools like Copilot, Amazon Q, and others may introduce new expenses. As these subscriptions evolve, it&#8217;s crucial to factor them into the overall economic calculation of software development costs.</p>



<h3 class="wp-block-heading">Limitations of AI-Assistance</h3>



<p>While AI can certainly augment human capabilities, there are still cases where traditional expertise is essential. For instance, when dealing with complex bugs or system design issues, human intuition and experience are invaluable assets.</p>



<h3 class="wp-block-heading">Velocity and Complexity</h3>



<p>It&#8217;s possible that the increased productivity in the implementation phase will lead to higher throughput and more story points delivered per capacity. However, this might also introduce new complexities that require reevaluation of estimation methods. Only time and experience will tell how these changes play out.</p>



<p>Ultimately, as AI-assisted software engineering becomes more prevalent, it&#8217;s crucial that we have enough capable engineers on hand to design, review, and maintain the generated code properly. Otherwise, there may be bottlenecks and no overall efficiency gain can be achieved. In the same direction, supporting only one activity in the whole software development life cycle will lead to bottlenecks or inefficiencies. It is crucial to apply AI in every phase to support engineers.</p>



<h2 class="wp-block-heading">Conclusion</h2>



<p>Please note that these thoughts are based on a somewhat idealistic and simplistic model. As a software developer myself, I am aware that our daily work does not always align with this scenario. Nevertheless, our work has undergone changes, and our understanding of the job must adapt too. One thing is certain: we require competent engineers like always.</p>



<p><sup><em>All my articles are written by myself. AI is used for some of the title images and in some cases to improve the wording and flow.</em></sup></p>
<p>The post <a href="https://ronnieschaniel.com/ai/the-cost-and-time-model-of-ai-assisted-software-engineering/">The Cost and Time Model of AI-Assisted Software Engineering</a> appeared first on <a href="https://ronnieschaniel.com">Ronnie Schaniel</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Gamify Software Engineering by letting AI rate your changes</title>
		<link>https://ronnieschaniel.com/ai/gamify-software-engineering-by-letting-ai-rate-your-changes/</link>
		
		<dc:creator><![CDATA[ronnieschaniel@hey.com]]></dc:creator>
		<pubDate>Sat, 24 Jan 2026 18:07:32 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<guid isPermaLink="false">https://ronnieschaniel.com/?p=2732</guid>

					<description><![CDATA[<p>Can you score high on the CoPilot code review? An example of how to gamify the AI usage.</p>
<p>The post <a href="https://ronnieschaniel.com/ai/gamify-software-engineering-by-letting-ai-rate-your-changes/">Gamify Software Engineering by letting AI rate your changes</a> appeared first on <a href="https://ronnieschaniel.com">Ronnie Schaniel</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="535" src="https://ronnieschaniel.com/wp-content/uploads/2026/01/gamify_software_engineering-1024x535.png" alt="" class="wp-image-2733" srcset="https://ronnieschaniel.com/wp-content/uploads/2026/01/gamify_software_engineering-1024x535.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2026/01/gamify_software_engineering-300x157.png 300w, https://ronnieschaniel.com/wp-content/uploads/2026/01/gamify_software_engineering-768x401.png 768w, https://ronnieschaniel.com/wp-content/uploads/2026/01/gamify_software_engineering.png 1408w" sizes="(max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Generated by Gemini.</figcaption></figure>



<p>Some developers are vibe-coding whole applications (if you believe them). Others are using AI coding assistants for a more intelligent autocomplete, to generate or edit some snippets, or just asking questions about code or other topics. And yet another group of software engineers are totally against AI coding assistants and rarely use them. Anyway, having your code reviewed by AI is usually not a bad idea. And gamifying that approach could motivate all of the above types.</p>



<h2 class="wp-block-heading">The prompt</h2>



<p>The goal for our review is to have an output considering different factors. Each factor should be rated from 1 to 10 in 0.5 increments. There should then be also actionable suggestion on how to improve the code. I have tried various variants and ended up with the following:</p>



<pre class="wp-block-code"><code lang="adoc" class="language-adoc">Assess the provided code based on the following criteria using a 1-10 scale:

Potential Bugs (1=high risk, 10=very low risk)
How likely is this code to introduce bugs or errors?

Tests (1=no tests, 10=comprehensive coverage)
Are there sufficient tests? Is the testing strategy robust?

Clarity (1=very unclear, 10=crystal clear)
How easy is it to understand the code's intent, logic, and structure through reading?

Consistency (1=very inconsistent, 10=fitting in perfectly)
How consistent is the added code. Are there new patterns in naming, design, style or is it very consistent with existing code?

Testability (1=very difficult to test, 10=highly testable)
Can the code be easily tested in isolation?

Design (1=poorly designed, 10=well-designed)
How well-architected is the code in terms of organization and modularity?

Security (1=high risk, 10=very low risk)
Are there potential security vulnerabilities?

Average Score: Calculate the average of the six criteria.

List the scores per category in a table with the average in the last row (always in the form of x/10). Provide the 2-5 most urgent suggestions to improve in a compact form. Ask the developer if more information about improvements are needed. You need to rate consistently throughout.</code></pre>



<p>This delivered for me the best results. I wanted to have a brief summary with only a few suggestions in the beginning. This would allow me to revisit the mentioned code myself and ask for more details if needed. Of course you could even ask AI to do the improvements itself.</p>



<h2 class="wp-block-heading">Review some bad code</h2>



<p>I have tried the above prompt on some code and got the following compact response:</p>



<figure class="wp-block-image size-large mt-5"><img decoding="async" width="774" height="1024" src="https://ronnieschaniel.com/wp-content/uploads/2026/01/ai_review_prompt_bad_example-774x1024.png" alt="" class="wp-image-2740" srcset="https://ronnieschaniel.com/wp-content/uploads/2026/01/ai_review_prompt_bad_example-774x1024.png 774w, https://ronnieschaniel.com/wp-content/uploads/2026/01/ai_review_prompt_bad_example-227x300.png 227w, https://ronnieschaniel.com/wp-content/uploads/2026/01/ai_review_prompt_bad_example-768x1016.png 768w, https://ronnieschaniel.com/wp-content/uploads/2026/01/ai_review_prompt_bad_example.png 1086w" sizes="(max-width: 774px) 100vw, 774px" /></figure>



<p>So here we have a lot of potential to improve.</p>



<h2 class="wp-block-heading">Scoring well</h2>



<p>After some improvements I can run the prompt again and I score now 8.3 on average, compared to the 4.6 before:</p>



<figure class="wp-block-image size-large mt-5"><img decoding="async" width="1024" height="910" src="https://ronnieschaniel.com/wp-content/uploads/2026/01/ai_review_prompt_after_improvements-1-1024x910.png" alt="" class="wp-image-2742" srcset="https://ronnieschaniel.com/wp-content/uploads/2026/01/ai_review_prompt_after_improvements-1-1024x910.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2026/01/ai_review_prompt_after_improvements-1-300x267.png 300w, https://ronnieschaniel.com/wp-content/uploads/2026/01/ai_review_prompt_after_improvements-1-768x683.png 768w, https://ronnieschaniel.com/wp-content/uploads/2026/01/ai_review_prompt_after_improvements-1.png 1118w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>Is the code now very good and I don&#8217;t need a human review at all? Definitely not! Even you should review again if the changes were done by AI. But once pushed to the remote your peers will probably be more happy about the Merge Request that should now look more pleasant than before.</p>



<h2 class="wp-block-heading">Make it reusable</h2>



<p>Finally we would like to have the prompt always ready. That is why you can create a .github/copilot/prompts folder with a prompt file codeReviewAssessment.prompt.md. The file is started with the following block and contains the prompt below that:</p>



<pre class="wp-block-code"><code lang="markdown" class="language-markdown">---
name: codeReviewAssessment
description: Evaluate code quality across seven dimensions with consistent scoring and actionable feedback.
argument-hint: The selected code or codebase to review
---</code></pre>



<p>This enables us to access the prompt by typing /codeReviewAssessment.</p>



<figure class="wp-block-image size-large mt-5"><img decoding="async" width="1024" height="205" src="https://ronnieschaniel.com/wp-content/uploads/2026/01/prompt_ready_for_code_review_assessment-1024x205.png" alt="" class="wp-image-2745" srcset="https://ronnieschaniel.com/wp-content/uploads/2026/01/prompt_ready_for_code_review_assessment-1024x205.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2026/01/prompt_ready_for_code_review_assessment-300x60.png 300w, https://ronnieschaniel.com/wp-content/uploads/2026/01/prompt_ready_for_code_review_assessment-768x154.png 768w, https://ronnieschaniel.com/wp-content/uploads/2026/01/prompt_ready_for_code_review_assessment.png 1098w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>
<p>The post <a href="https://ronnieschaniel.com/ai/gamify-software-engineering-by-letting-ai-rate-your-changes/">Gamify Software Engineering by letting AI rate your changes</a> appeared first on <a href="https://ronnieschaniel.com">Ronnie Schaniel</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>50 AI Mini Use Cases for Software Engineers: Boost Your Productivity with GPT and Coding Assistants</title>
		<link>https://ronnieschaniel.com/ai/50-ai-mini-use-cases-for-software-engineers-boost-your-productivity-with-gpt-and-coding-assistants/</link>
		
		<dc:creator><![CDATA[ronnieschaniel@hey.com]]></dc:creator>
		<pubDate>Sun, 21 Dec 2025 13:02:44 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<guid isPermaLink="false">https://ronnieschaniel.com/?p=2651</guid>

					<description><![CDATA[<p>The key is to be creative.</p>
<p>The post <a href="https://ronnieschaniel.com/ai/50-ai-mini-use-cases-for-software-engineers-boost-your-productivity-with-gpt-and-coding-assistants/">50 AI Mini Use Cases for Software Engineers: Boost Your Productivity with GPT and Coding Assistants</a> appeared first on <a href="https://ronnieschaniel.com">Ronnie Schaniel</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-full"><img decoding="async" width="1022" height="781" src="https://ronnieschaniel.com/wp-content/uploads/2025/12/mini_ai_use_cases_as_software_engineer.png" alt="" class="wp-image-2653" srcset="https://ronnieschaniel.com/wp-content/uploads/2025/12/mini_ai_use_cases_as_software_engineer.png 1022w, https://ronnieschaniel.com/wp-content/uploads/2025/12/mini_ai_use_cases_as_software_engineer-300x229.png 300w, https://ronnieschaniel.com/wp-content/uploads/2025/12/mini_ai_use_cases_as_software_engineer-768x587.png 768w" sizes="(max-width: 1022px) 100vw, 1022px" /><figcaption class="wp-element-caption">This picture was generated by Gemini.</figcaption></figure>



<p>The advent of General-Purpose Transformational (GPT) models, followed by coding assistants like GitHub Copilot, has revolutionized the software engineering landscape. However, many developers struggle to go beyond basic use cases. A bit of creativity is needed. To unleash the full potential of AI in your daily work, I&#8217;ve compiled a comprehensive list of 50 AI mini use cases that you can apply using general GPT or a coding assistant.</p>



<p>Before diving into these use cases, please keep two essential points in mind: first, ensure you have the necessary permissions to input information into the chat bot from a security perspective; and second, always review the output generated by the AI tool.</p>



<h2 class="wp-block-heading">GPT Use Cases</h2>



<p>This is for use cases outside of your integrated development environment (IDE). It contains simple things that you probably did manually in the past. The order is random.</p>



<ol class="wp-block-list numeric-list">
<li><strong>Translate</strong>: use it to understand any text.</li>



<li><strong>Test data generation:</strong> based on classes or examples.</li>



<li><strong>Dashboard explanation:</strong> export a monitoring or analytics dashboard source. Paste it into the chat and ask it to describe the dashboard in natural language, including where the data comes from and how numbers are calculated.</li>



<li><strong>Query parameter generation:</strong> paste a JSON and ask it for transforming into a query parameter string.</li>



<li><strong>Test scenarios:</strong> generate test scenarios based on a feature description.</li>



<li><strong>Drafting documents:</strong> draft documentation and manuals.</li>



<li><strong>SQL queries</strong>: write or optimise SQL queries.</li>



<li><strong>Write queries for your monitoring system</strong>, e.g. Splunk or Grafana.</li>



<li><strong>Summarise feedback</strong>: textual user feedback and suggest improvement ideas.</li>



<li><strong>Release notes: </strong>Generate release notes based on GIT commits.</li>



<li><strong>Scripts: </strong>Write simple scripts.</li>



<li><strong>Meeting notes:</strong> Summarise meeting notes and create a task/action list.</li>



<li><strong>Data validation rules</strong>: Given a dataset schema, generate a set of data validation rules (e.g., 3-5 rules) to ensure the data meets certain criteria and is consistent with the schema.</li>



<li><strong>Draft presentations:</strong> create a first version or an outline for a presentation.</li>



<li><strong>Review architectures:</strong> describe how you plan to implement a feature on the architectural level to get feedback, descriptions or diagrams.</li>



<li><strong>Generate checklists:</strong> create checklists for deployments, releases or other critical tasks.</li>



<li><strong>Evaluate skills:</strong> explain your current skills and ask for an evaluation and list of gaps to guide your next learning steps.</li>



<li><strong>Summarise technical articles</strong>: to learn about topics.</li>



<li><strong>Quizzes:</strong> generate a quiz based on a technical article or documentation.</li>



<li><strong>Interview preparation:</strong> ask for potential questions based on a job description.</li>



<li><strong>Create survey or forms:</strong> generate questions and answer options. Also use it later to summarise replies.</li>



<li><strong>Glossary:</strong> generate a list of terms that are not generally understandable.</li>



<li><strong>Generate the agenda for a meeting:</strong> draft the meeting invitation.</li>



<li><strong>Explain a dataset:</strong> take a dataset and explain the latest trends and highlights.</li>



<li><strong>Vulnerability analysis</strong>: describe a setup or solution and ask for a threat modelling and potential vulnerabilities.</li>
</ol>



<p>These were the general chat use cases. Some of them could also be done in the IDE directly of course. Anyway, in the next chapter you find use cases that are more typical to be directly executed by your AI coding assistant.</p>



<h2 class="wp-block-heading">Coding Assistant Use Cases</h2>



<p>The use cases below are all about using CoPilot and co. in a way to help you working.</p>



<ol class="wp-block-list numeric-list">
<li><strong>User Story to code:</strong> copy a complete User Story description into the chat, use agent mode, and ask it to generate the code for the story.</li>



<li><strong>Review code changes:</strong> at the end of implementing a change, ask the coding assistant to review your changes for security, clarity, potential bugs or other issues.</li>



<li><strong>Visualise logic: </strong>open a class or module that contains lots of logic. Ask to generate a <a href="https://www.mermaidchart.com/" target="_blank" rel="noreferrer noopener">mermaid</a> diagram definition (text based diagram).</li>



<li><strong>Regex:</strong> ask to generate a regex based on a textual description or examples.</li>



<li><strong>Generate specific values:</strong> generate UUIDs or other random data that you occasionally need.</li>



<li><strong>Tests: </strong>ask to generate unit tests for a certain class or module.</li>



<li><strong>TDD: </strong>generate the implementation based on tests.</li>



<li><strong>Test coverage:</strong> ask which cases are not tested yet.</li>



<li><strong>Error explanation:</strong> paste a stack trace of an error and ask for a fix.</li>



<li><strong>Data transformation:</strong> transform data from one representation to another, e.g. JSON to TypeScript.</li>



<li><strong>Document code:</strong> ask to generate a documentation based on code.</li>



<li><strong>3rd party library:</strong> get information about a third party library that you want to use.</li>



<li><strong>Review accessibility:</strong> review html code for accessibility issues or make it compliant.</li>



<li><strong>Technology migration:</strong> switch from one implementation version to another, e.g. Java 20 to 21, RestTemplate to WebClient.</li>



<li><strong>Improve performance:</strong> ask about performance issues in the code and ways to improve.</li>



<li><strong>Understand:</strong> explain code.</li>



<li><strong>Port between languages:</strong> transform code from one language into another.</li>



<li><strong>Generate commit messages:</strong> generate git commit messages based on the changes.</li>



<li><strong>Overview</strong>: ask for a general overview of a project&#8217;s source code if it&#8217;s new to you to understand conventions, patterns and libraries used.</li>



<li><strong>Refactoring to patterns: </strong>if you see a code smell and the possibility to refactor to a design pattern, use the coding assistant.</li>



<li><strong>Generate performance tests:</strong> create performance tests, e.g. JMeter scripts, based on the APIs.</li>



<li><strong>Optimise internal exceptions and error:</strong> draft text for internal errors and exception for your fellow engineers and your future self.</li>



<li><strong>Generate API requests</strong>: create API requests in curl, postman, bruno or any other API tool.</li>



<li><strong>README:</strong> generate READMEs based on the code.</li>



<li><strong>Improvement and refactoring</strong>: ask for potential changes to your code.</li>
</ol>



<p>These use cases are designed to help you unlock the full potential of GPT and coding assistants in your daily workflow. Whether you&#8217;re a seasoned software engineer or just starting out, these mini use cases will inspire you to find innovative solutions to common challenges.</p>
<p>The post <a href="https://ronnieschaniel.com/ai/50-ai-mini-use-cases-for-software-engineers-boost-your-productivity-with-gpt-and-coding-assistants/">50 AI Mini Use Cases for Software Engineers: Boost Your Productivity with GPT and Coding Assistants</a> appeared first on <a href="https://ronnieschaniel.com">Ronnie Schaniel</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Make your Gen AI Project a Success</title>
		<link>https://ronnieschaniel.com/ai/make-your-gen-ai-project-a-success/</link>
		
		<dc:creator><![CDATA[ronnieschaniel@hey.com]]></dc:creator>
		<pubDate>Sun, 21 Dec 2025 09:03:20 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<guid isPermaLink="false">https://ronnieschaniel.com/?p=2628</guid>

					<description><![CDATA[<p>How to design, implement and roll out Generative AI solutions that deliver value.</p>
<p>The post <a href="https://ronnieschaniel.com/ai/make-your-gen-ai-project-a-success/">Make your Gen AI Project a Success</a> appeared first on <a href="https://ronnieschaniel.com">Ronnie Schaniel</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="572" src="https://ronnieschaniel.com/wp-content/uploads/2025/12/usage_of_AI_in_projects-1024x572.png" alt="" class="wp-image-2627" title="Generated by Gemini" srcset="https://ronnieschaniel.com/wp-content/uploads/2025/12/usage_of_AI_in_projects-1024x572.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2025/12/usage_of_AI_in_projects-300x167.png 300w, https://ronnieschaniel.com/wp-content/uploads/2025/12/usage_of_AI_in_projects-768x429.png 768w, https://ronnieschaniel.com/wp-content/uploads/2025/12/usage_of_AI_in_projects.png 1376w" sizes="(max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">This picture was generated by Gemini.</figcaption></figure>



<p>The release of ChatGPT in November 2022 sparked excitement about the possibilities of Generative AI. In just five days, it gained over a million users. As businesses began to explore how to leverage this technology, they quickly realized that using AI internally was crucial.</p>



<p>Fast-forward three years, and many companies have jumped on the Generative AI bandwagon, building their own GPT or LLM-based solutions. However, what I often see is that creators simply add an LLM with Retrieval Augmented Generation (RAG) to their data, hoping for the best. But does this approach truly deliver value? I have some doubts.</p>



<p>This sentiment is also confirmed by various studies, MITs &#8220;<a href="https://api.directual.com/fileUploaded/directual-site/e8ed57ab-19a8-4834-815f-9109a4c06f0e.pdf" target="_blank" rel="noreferrer noopener">State of AI in Business 2025</a>&#8221; reports that &#8220;Despite $30–40 billion in enterprise investment into GenA (&#8230;) 95% of organizations are getting zero return&#8221;. In this post we speak about software companies or companies that have at least their own IT department and are planning to build their own AI solutions.</p>



<h2 class="wp-block-heading">The naive approach for Generative AI projects</h2>



<p>Cost pressure is high in many tech companies. And so is the hype around AI. An easy strategy is therefore to just invest in AI. An LLMs capability can look impressive, but the main issues is that it doesn&#8217;t have access to company data out of the box. Retrieval augmented generation can help there and extend the LLMs context by data in files or on wiki pages. It&#8217;s also not super difficult to implement. So, many just start to RAGify everything they have. This is nice to have and the use cases around it are many, but:</p>



<ul class="wp-block-list">
<li>The data that is retrieved is often in bad quality and can be outdated.</li>



<li>As the use cases are endless, end-users struggle to understand how to exactly use the new possibilities. I&#8217;ve got that wiki page no in a chat bot, nice, but what now? What should I do with it?</li>



<li>Even if there is a chat bot available somewhere backed by RAG, it&#8217;s usually a standalone app. This requires users to switch context and leads to a fragmented tool landscape.</li>



<li>The return on investment (ROI) of such solutions is not measured or not understood. </li>



<li>Projects look fancy in the beginning because you get impressive answers very soon. But optimising the solution to reach production state is hard. Project teams often miss to have an evaluation pipeline and defined KPIs.</li>



<li>Big use cases are tackled early on and take months to implement, delaying the benefits and in the worst case leading to a solution that is not usable at all.</li>



<li>The core of the AI solution might be built within a few days. But integrating it into the existing system landscape, considering security, compliance, people training, data maintenance, and other factors can take months.</li>
</ul>



<p>Still the pressure remain, we have to do something with AI! And in the next section I would like to highlight a few points that help you to make your project a success.</p>



<h2 class="wp-block-heading">How to tackle Generative AI projects</h2>



<p>I&#8217;m convinced that AI has benefits in it for most businesses. How do we make it a success? First of all, slow down! Don&#8217;t follow the latest trends daily, but have a robust AI strategy that is dynamic enough to adapt, but doesn&#8217;t lead to jumping always onto the latest technological advances in the field of AI. Because there&#8217;s just too much going on.</p>



<p>And then more importantly. Don&#8217;t take Generative AI and RAG as a technology and ask yourself what it can solve. That&#8217;s the wrong direction! Ask what problems do you have in your company and design a solution for it. And then ask what parts of the solution can be covered or improved by generative AI.</p>



<p>Also don&#8217;t forget how software was built in the last years. Don&#8217;t forget agile! Put the user, feedback and short iterations into the center.</p>



<p>Consider the following when you&#8217;re planning to build an AI solution in your organisation:</p>



<ul class="wp-block-list">
<li>Start with problems, not technology: Design a solution for specific company challenges, and then ask how AI can help.</li>



<li>Use agile principles as in any other software product you have built in the last years.</li>



<li>Don&#8217;t build standalone solutions, but make sure AI is integrated into the existing workflows.</li>



<li>Educate and train the user. This starts even by making sure the AI strategy is understood by everyone and nobody has to fear for their jobs.</li>



<li>As AI is non-deterministic, define KPIs that you need to achieve to make your solution useful. Iterate and improve.</li>



<li>Define the ROI you want to see when you plan the solution. What is your business case? What can your colleagues do with the solution you&#8217;re building?</li>



<li>Consider data management, security, compliance, and other non-functional requirements early!</li>
</ul>



<p>Finally, you might put a generative AI solution into production that makes some work faster or better. Ask yourself what the time freed up by that, can be used for. If you make a process faster only to run into waiting times or have colleagues who can&#8217;t use their newly won time, you&#8217;ve failed too.</p>



<p></p>
<p>The post <a href="https://ronnieschaniel.com/ai/make-your-gen-ai-project-a-success/">Make your Gen AI Project a Success</a> appeared first on <a href="https://ronnieschaniel.com">Ronnie Schaniel</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>RxJS Mastery &#8211; How to build streams</title>
		<link>https://ronnieschaniel.com/rxjs/rxjs-mastery-how-to-build-streams/</link>
		
		<dc:creator><![CDATA[ronnieschaniel@hey.com]]></dc:creator>
		<pubDate>Mon, 11 Mar 2024 16:05:53 +0000</pubDate>
				<category><![CDATA[RxJS]]></category>
		<category><![CDATA[RxJs Lessons]]></category>
		<guid isPermaLink="false">https://ronnieschaniel.com/?p=2367</guid>

					<description><![CDATA[<p>Use RxJS without frustration. Build your streams successfully step-by-step.</p>
<p>The post <a href="https://ronnieschaniel.com/rxjs/rxjs-mastery-how-to-build-streams/">RxJS Mastery &#8211; How to build streams</a> appeared first on <a href="https://ronnieschaniel.com">Ronnie Schaniel</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="392" src="https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_mastery_how_to_build_streams-1024x392.png" alt="" class="wp-image-2369" srcset="https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_mastery_how_to_build_streams-1024x392.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_mastery_how_to_build_streams-300x115.png 300w, https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_mastery_how_to_build_streams-768x294.png 768w, https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_mastery_how_to_build_streams-1536x588.png 1536w, https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_mastery_how_to_build_streams.png 1660w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>I have often seen people struggling when using RxJS. They are frustrated because they try too much at once, do not have proper tests, or have difficulties understanding error messages. How can you do RxJS development effectively and efficiently? I give my view on that in this article. Done right, RxJS development is easy because the library offers a lot of solutions for the usually difficult asynchronous programming.</p>



<h2 class="wp-block-heading">How to develop RxJS code</h2>



<h3 class="wp-block-heading">Mental modal for RxJS</h3>



<p>When working with RxJS it is important to have the right ideas about it. Seeing your code as a collection of streams of values can help. On that value stream, you can apply simple operators. Each stream you are dealing with can start, error out, or complete.</p>



<h3 class="wp-block-heading">Controlled step-by-step approach with verification</h3>



<p>Streams can be complex. That means they can have branches, multiple values arriving at different times, or nested structures. And that is why one has to build them step-by-step and include some verification into the process:</p>



<ul class="wp-block-list">
<li>You can effectively build streams by using TDD (Test-Driven Development)</li>



<li>Keep Observables as flat as possible (avoid nesting where possible)</li>



<li>Use Typescript properly to achieve additional safety and add more clarity inside streams</li>



<li>Understand compiler and error messages when something goes wrong</li>
</ul>



<h3 class="wp-block-heading">Use reactive code only when necessary</h3>



<p>You do not need to cover everything in RxJS. If something is not asynchronous, RxJS is the wrong choice. Keep your streams small and understandable and implement synchronous parts of your application with the right pattern, i.e. in synchronous code. Asynchronous code is hard enough on its own, despite RxJS.</p>



<h2 class="wp-block-heading">Example case</h2>



<p>Let us illustrate the above considerations in an example. In our demo case, we would like to load a Post entity based on a user action. To load that we also need to pass the user ID which is also offered as Observable. To spice things up, we also would like to load the post&#8217;s comments after the post is loaded. </p>



<h3 class="wp-block-heading">Setup testing properly</h3>



<p>The start is a simple test with an implementation you could choose to load the post. I have seen code similar to the following one many times. And I have seen developers struggling with it, being frustrated, and in the end, they blame RxJS.</p>



<pre class="wp-block-code"><code lang="typescript" class="language-typescript">it('does not work', () =&gt; {
    const action$: Observable&lt;LoadPostsAction&gt; = of({ postId: 1 });
    const postService = new PostService();

    action$.pipe(
        map(action =&gt; action.postId),
        concatMap((postId) =&gt; postService.loadPost(postId)),
    ).subscribe({
        next: result =&gt; {
            expect(result).toEqual({id: 1, content: 'content of post 2'});
        },
    });
});</code></pre>



<p>If we run that test, all looks good:</p>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="187" src="https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_how_to_build_stream_test_passes_wrongly-1024x187.png" alt="All looks fine in the test." class="wp-image-2376" srcset="https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_how_to_build_stream_test_passes_wrongly-1024x187.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_how_to_build_stream_test_passes_wrongly-300x55.png 300w, https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_how_to_build_stream_test_passes_wrongly-768x140.png 768w, https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_how_to_build_stream_test_passes_wrongly.png 1358w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>The above code looks perfectly fine at first sight. But it is not! The test is green although the assertion is wrong (&#8220;post 2&#8221; although we would get &#8220;post 1&#8221; back in the result). Time for our first tip:</p>



<p><strong>☝</strong> <strong>Tip #1</strong>: Use TDD when developing RxJS streams and let the test first fail.</p>



<p>This means that in our case we first need a proper test setup. When testing asynchronous code in Jest (and alternatives), we need a callback. This is usually defined as &#8220;done&#8221; and we are then invoking the <code>done()</code> callback after asserting the outcome.</p>



<pre class="wp-block-code"><code lang="typescript" class="language-typescript">it('does not work', (done) =&gt; {
    const action$: Observable&lt;LoadPostsAction&gt; = of({ postId: 1 });
    const postService = new PostService();

    action$.pipe(
        map(action =&gt; action.postId),
        concatMap((postId) =&gt; postService.loadPost(postId)),
    ).subscribe({
        next: result =&gt; {
            expect(result).toEqual({id: 1, content: 'content of post 2'});
            done();
        },
    });
});</code></pre>



<p>We have a failing test and are good to go and start extending our stream:</p>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="248" src="https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_how_to_build_streams_test_correctly-1024x248.png" alt="" class="wp-image-2383" srcset="https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_how_to_build_streams_test_correctly-1024x248.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_how_to_build_streams_test_correctly-300x73.png 300w, https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_how_to_build_streams_test_correctly-768x186.png 768w, https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_how_to_build_streams_test_correctly-1536x372.png 1536w, https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_how_to_build_streams_test_correctly.png 1592w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>Of course, there are other testing approaches, e.g. marbles. But I don&#8217;t want to open that topic here. You can read my blog post about <a href="https://ronnieschaniel.com/rxjs/rxjs-mastery-testing-approaches-compared/">different RxJS testing approaches</a> if you are interested. Anyway, we need to be sure that we are testing the right thing and that is why our test should fail first, independent of the testing approach!</p>



<h3 class="wp-block-heading">Have the right mental model</h3>



<p>Before we move on let us spend some time to see how you as a developer can think about RxJS streams. The mental model I propose asks us to think about the values and how they stream through our program. We can illustrate this as follows:</p>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="517" src="https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_how_to_build_streams_mental_modal_1-1-1024x517.png" alt="" class="wp-image-2394" srcset="https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_how_to_build_streams_mental_modal_1-1-1024x517.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_how_to_build_streams_mental_modal_1-1-300x151.png 300w, https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_how_to_build_streams_mental_modal_1-1-768x388.png 768w, https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_how_to_build_streams_mental_modal_1-1-1536x775.png 1536w, https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_how_to_build_streams_mental_modal_1-1-2048x1033.png 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>Values are green, source Observables blue (they are not the only Observables), and operators are indicated as rounded rectangles. Arrows show the flow of values.</p>



<p><strong>☝</strong> <strong>Tip #2</strong>: Remember that Observables are just values over time and those values flow through your program.</p>



<p>Now let us extend the functionality safely.</p>



<h3 class="wp-block-heading">Keep Observables as flat as possible</h3>



<p>Let us now tackle the next step of our functionality and load posts by user. For that, we need to take the current user ID into account. RxJS would allow nesting Observables. What you do then is branching of streams. But besides that, you also make code harder to read and harder to change. So it is not advisable to implement the new functionality like this (switchMap is nested inside concatMap):</p>



<pre class="wp-block-code"><code lang="typescript" class="language-typescript">action$.pipe(
    map(action =&gt; action.postId),
    concatMap((postId) =&gt; {
        return user.getCurrentUserId$().pipe(
            switchMap((userId) =&gt; postService.loadPost(postId, userId))
        );
    })
).subscribe({
    next: result =&gt; {
        expect(result).toEqual({id: 1, content: 'content of post 1'});
        done();
    },
});</code></pre>



<p>The <code>user.getCurrentUserId$()</code> Observable is used inside concatMap. This also means that we need to flatten the Observable of <code>loadPost</code> (more towards that in the next sub-section). A flat and clean structure is easier to handle:</p>



<pre class="wp-block-code"><code lang="typescript" class="language-typescript">action$.pipe(
    map(action =&gt; action.postId),
    withLatestFrom(user.getCurrentUserId$()),
    concatMap((postId) =&gt; postService.loadPost(postId, null)),
).subscribe({
    next: result =&gt; {
        expect(result).toEqual({id: 1, content: 'content of post 1'});
        done();
    },
});</code></pre>



<p>Whenever the <code>action$</code> delivers a new value we want to combine it with the latest value of the current user&#8217;s ID. You see that we kept the null for <code>userId</code> in the <code>loadPost</code> method. This is because we want to first see what the compiler tells us (to showcase the importance of error messages).</p>



<h3 class="wp-block-heading">Understand error messages</h3>



<p>Error messages around RxJS can be hard to understand in the beginning. The last code example from above delivers the following on the <code>postId</code> parameter inside the <code>loadPost</code> method call:</p>



<pre class="wp-block-code"><code lang="bash" class="language-bash">TS2345: Argument of type '[number, number]' is not assignable to parameter of type 'number'.</code></pre>



<p>The method expects a number but receives <code>[number, number]</code>. We have to understand <code><a href="https://rxjs.dev/api/index/function/pipe" target="_blank" rel="noreferrer noopener">pipe</a></code> here. Pipe is accepting operators as <code>UnaryFunction</code>s and forwards the output of one operator to the subsequent one. This passed output is one value, object, or array. Calling withLatestFrom changes the values in the stream and transforms <code>number</code> to <code>[number, number]</code>, representing the <code>postId</code> and <code>userId</code> in an array. Long story short, to fix the above TS2345 you have to adapt the code to consider the new input to the operator:</p>



<pre class="wp-block-code"><code lang="typescript" class="language-typescript">action$.pipe(
    map(action =&gt; action.postId),
    withLatestFrom(user.getCurrentUserId$()),
    concatMap(([postId, userId]) =&gt; postService.loadPost(postId, userId)),
).subscribe({
    next: result =&gt; {
        expect(result).toEqual({id: 1, content: 'content of post 1'});
        done();
    },
});</code></pre>



<p>And please don&#8217;t use <code>(postId, userId)</code>. Something like this is not a <a href="https://rxjs.dev/api/index/interface/UnaryFunction" target="_blank" rel="noreferrer noopener">UnaryFunction</a> and is not going to work! This error message was not directly provided by RxJS but often occurs during RxJS development.</p>



<p>Another error message one often encounters is about higher-order Observables. Let us go back to the nested example from above. It is easy to miss the higher-order mapping operator, i.e. <code>switchMap</code>, and just use <code>map</code> in place of it:</p>



<pre class="wp-block-code"><code lang="typescript" class="language-typescript">action$.pipe(
    map(action =&gt; action.postId),
    concatMap((postId) =&gt; {
        return user.getCurrentUserId$().pipe(
            map((userId) =&gt; postService.loadPost(postId, userId))
        );
    })
).subscribe({
    next: result =&gt; {
        expect(result).toEqual({id: 1, content: 'content of post 1'});
        done();
    },
});</code></pre>



<p>All looks good at first sight, but the test signals us:</p>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="212" src="https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_hot_to_build_streams_wrong_nesting-1024x212.png" alt="" class="wp-image-2397" srcset="https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_hot_to_build_streams_wrong_nesting-1024x212.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_hot_to_build_streams_wrong_nesting-300x62.png 300w, https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_hot_to_build_streams_wrong_nesting-768x159.png 768w, https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_hot_to_build_streams_wrong_nesting-1536x318.png 1536w, https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_hot_to_build_streams_wrong_nesting-2048x424.png 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>We are expecting an object with fields like content and id. But what we are getting is not such an Object, but an Observable. Whenever you see this, you should check if you need to flatten another Observable. In our case, <code>switchMap</code> flattens an Observable.</p>



<p><strong>☝</strong> <strong>Tip #3:</strong> Try to understand each error message. They look cryptic and chaotic at times but are really not that hard to understand once you read them carefully. Go through them from top to bottom and focus on the bottom as the root cause is usually hidden there. During development with RxJS errors can happen and you should not guess, but read, and understand.</p>



<p>Update to our value stream model:</p>



<figure data-wp-context="{&quot;imageId&quot;:&quot;69d45ec596293&quot;}" data-wp-interactive="core/image" data-wp-key="69d45ec596293" class="wp-block-image size-large wp-lightbox-container"><img decoding="async" width="1024" height="442" data-wp-class--hide="state.isContentHidden" data-wp-class--show="state.isContentVisible" data-wp-init="callbacks.setButtonStyles" data-wp-on--click="actions.showLightbox" data-wp-on--load="callbacks.setButtonStyles" data-wp-on-window--resize="callbacks.setButtonStyles" src="https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_how_to_build_streams_mental_model_2-1024x442.png" alt="" class="wp-image-2400" srcset="https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_how_to_build_streams_mental_model_2-1024x442.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_how_to_build_streams_mental_model_2-300x130.png 300w, https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_how_to_build_streams_mental_model_2-768x332.png 768w, https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_how_to_build_streams_mental_model_2-1536x663.png 1536w, https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_how_to_build_streams_mental_model_2-2048x884.png 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /><button
			class="lightbox-trigger"
			type="button"
			aria-haspopup="dialog"
			aria-label="Enlarge"
			data-wp-init="callbacks.initTriggerButton"
			data-wp-on--click="actions.showLightbox"
			data-wp-style--right="state.imageButtonRight"
			data-wp-style--top="state.imageButtonTop"
		>
			<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 12 12">
				<path fill="#fff" d="M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z" />
			</svg>
		</button></figure>



<h3 class="wp-block-heading">Use types</h3>



<p>As soon as values become more complex it can help to use types explicitly. True, the Typescript compiler can still help us and infer some types. Nevertheless specifying them explicitly can make it easier for us developers to understand. Extending by types will change our code to:</p>



<pre class="wp-block-code"><code lang="typescript" class="language-typescript">action$.pipe(
    map((action: LoadPostAction) =&gt; action.postId),
    withLatestFrom(user.getCurrentUserId$()),
    concatMap(([postId, userId]: [number, number]) =&gt; postService.loadPost(postId, userId)),
).subscribe({
    next: (result: Post) =&gt; {
        expect(result).toEqual({id: 1, content: 'content of post 1'});
        done();
    },
});</code></pre>



<p>This makes extensions safer. For example, if we map the response of the post service to the content only (return string instead of Post), we would immediately notice because we also typed the result in the subscribe&#8217;s next handler. <br>The first one here looks okay to the compiler. Although we are passing string (the type of content) instead of Post.</p>



<pre class="wp-block-code"><code lang="typescript" class="language-typescript">action$.pipe(
    map((action: LoadPostAction) =&gt; action.postId),
    withLatestFrom(user.getCurrentUserId$()),
    concatMap(([postId, userId]: [number, number]) =&gt; postService.loadPost(postId, userId)),
    map((response: Post) =&gt; response.content),
).subscribe({
    next: (result) =&gt; {
        expect(result).toEqual({id: 1, content: 'content of post 1'});
        done();
    },
});</code></pre>



<p>As soon as the next handler does have a type for its parameter, the compiler complains:</p>



<figure data-wp-context="{&quot;imageId&quot;:&quot;69d45ec5967e9&quot;}" data-wp-interactive="core/image" data-wp-key="69d45ec5967e9" class="wp-block-image size-large is-resized wp-lightbox-container"><img decoding="async" width="1024" height="275" data-wp-class--hide="state.isContentHidden" data-wp-class--show="state.isContentVisible" data-wp-init="callbacks.setButtonStyles" data-wp-on--click="actions.showLightbox" data-wp-on--load="callbacks.setButtonStyles" data-wp-on-window--resize="callbacks.setButtonStyles" src="https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_now_to_build_streams_wrong_output_to_next-1-1024x275.png" alt="" class="wp-image-2405" style="width:839px;height:auto" srcset="https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_now_to_build_streams_wrong_output_to_next-1-1024x275.png 1024w, https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_now_to_build_streams_wrong_output_to_next-1-300x81.png 300w, https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_now_to_build_streams_wrong_output_to_next-1-768x207.png 768w, https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_now_to_build_streams_wrong_output_to_next-1-1536x413.png 1536w, https://ronnieschaniel.com/wp-content/uploads/2024/02/rxjs_now_to_build_streams_wrong_output_to_next-1-2048x551.png 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /><button
			class="lightbox-trigger"
			type="button"
			aria-haspopup="dialog"
			aria-label="Enlarge"
			data-wp-init="callbacks.initTriggerButton"
			data-wp-on--click="actions.showLightbox"
			data-wp-style--right="state.imageButtonRight"
			data-wp-style--top="state.imageButtonTop"
		>
			<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 12 12">
				<path fill="#fff" d="M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z" />
			</svg>
		</button></figure>



<p>If you used RxJS before you probably have seen such error messages already. Important here is to understand that the error message is also considering the nesting. That is why we should always go to the innermost part of the message because that is usually the part the easiest to understand. In this case, &#8220;Type &#8216;string&#8217; is not assignable to type &#8216;Post'&#8221;. You see that TypeScript informs us quite well about the error. We expect a Post, but we are getting something of type string. The DX around error messages could be improved though.</p>



<p><strong>☝</strong> <strong>Tip #4: </strong>some extra types can help to make your code more robust and understand errors earlier. The compiler is a cheap and fast way to &#8220;test&#8221;, please use it! </p>



<p>Of course, there are cases where explicit types make no sense and we should really on the type inference instead.</p>



<h2 class="wp-block-heading">Conclusion</h2>



<p>I hope those hints give you a better understanding and you can optimize your approach to RxJS development. Having the right mental model about your stream helps to understand what the operators do and what is happening to the values flowing through your program over time. </p>



<p></p>
<p>The post <a href="https://ronnieschaniel.com/rxjs/rxjs-mastery-how-to-build-streams/">RxJS Mastery &#8211; How to build streams</a> appeared first on <a href="https://ronnieschaniel.com">Ronnie Schaniel</a>.</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
