<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nico Krijnen</title>
    <description>The latest articles on DEV Community by Nico Krijnen (@realworldarchitect).</description>
    <link>https://dev.to/realworldarchitect</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3638558%2Fd60225da-8ae5-445e-b815-59692c6703fc.jpg</url>
      <title>DEV Community: Nico Krijnen</title>
      <link>https://dev.to/realworldarchitect</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/realworldarchitect"/>
    <language>en</language>
    <item>
      <title>Programming as Theory Building</title>
      <dc:creator>Nico Krijnen</dc:creator>
      <pubDate>Mon, 07 Sep 2026 09:29:29 +0000</pubDate>
      <link>https://dev.to/realworldarchitect/programming-as-theory-building-4b3e</link>
      <guid>https://dev.to/realworldarchitect/programming-as-theory-building-4b3e</guid>
      <description>&lt;p&gt;Picture, you join a new team working on a big system. Everybody who knew anything has left, either to find greener grass or to enjoy a well deserved pension. You and the team struggle to build new features for the system or to adapt functionality to match changes in legislation. Not to mention the trouble it is to figure out what to fix when things go wrong. At the same time, the business that you support is screaming for innovation and pushing for more and more changes.&lt;/p&gt;

&lt;p&gt;Recognize this situation? Ever experienced it yourself?&lt;/p&gt;

&lt;h2&gt;
  
  
  A world full of legacy systems
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;“Legacy. What is a legacy? It’s planting seeds in a garden you never get to see.”&lt;/p&gt;

&lt;p&gt;– Lin-Manuel Miranda, “Hamilton”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Legacy, the thing that you are remembered for, typically the word has a positive meaning… how come that in tech the word “Legacy” has such a bad connotation? When we call out a legacy system, we usually mean: code without tests (&lt;a href="https://understandlegacycode.com/blog/what-is-legacy-code-is-it-code-without-tests/" rel="noopener noreferrer"&gt;Michael Feathers&lt;/a&gt;) or code you “got” from somebody else, or code that you’re scared to touch.&lt;/p&gt;

&lt;p&gt;However, there is a reason these legacy systems are still around. In almost all cases, that system still brings in money or is somehow still valuable. If it did not bring any value anymore, wouldn’t it be decommissioned? There must be something in these systems that makes them survive, where other systems did not.&lt;/p&gt;

&lt;h2&gt;
  
  
  How systems become “Legacy”
&lt;/h2&gt;

&lt;p&gt;So legacy systems are those that have become hard or scary to change. In my experience, that not because something is wrong with the code or technology. The major contributing factor is usually that the &lt;strong&gt;knowledge about the system has left&lt;/strong&gt; the organization. And then I don’t mean the documentation, but the people that built, maintained and ran the system. When those people are gone, you know that nobody else is going to be happy touching that thing.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/kqLz53z8Inc" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The value of software
&lt;/h2&gt;

&lt;p&gt;Code is like a mapping of desired real world behavior to a program that can be executed by a machine. So where is the value of a system, is that in that code?&lt;/p&gt;

&lt;p&gt;Over the past years I’ve talked a lot at conferences and training workshops about the value of code. How I used to explain it is that the &lt;strong&gt;value of software is to be easy to change&lt;/strong&gt;. It’s in the name: soft = flexible. Which contrasts with hardware, which is quite hard to change once you’ve soldered everything together. Pretty much all the things that we do to improve our way of writing code boil down to making the code easier to understand for the next person working on it (or yourself in 6 months). Our primary objective for building maintainable systems is to make code easy to change. In a sense, to make the “legacy” that we leave behind be a positive one.&lt;/p&gt;

&lt;p&gt;Then, about a year ago, I got drawn deep into the rabbit hole by this video on &lt;a href="https://youtu.be/Y6ZHV0RH0fQ" rel="noopener noreferrer"&gt;The Value of Source Code&lt;/a&gt; by &lt;a href="https://vonfranque.com/" rel="noopener noreferrer"&gt;Philomatics&lt;/a&gt;. It stated that the value of a software system does not have a lot to doe with the actual source code. Instead, the real value is the ability to quickly change the system, and that the key thing that you need for that is people with a “ &lt;strong&gt;mental theory”&lt;/strong&gt; of why the system works the way it does. Without that theory, no person is able to confidently make any changes.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Y6ZHV0RH0fQ" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;For me, this way of looking at it immediately made sense. When I look back at observations I made on the numerous software systems I worked on over the years, everything just fell into place. I also realized that this fundamental shift in thinking about software systems has wide implications. Or as &lt;a href="https://medium.com/@reymalena/lets-read-peter-naur-s-programming-as-theory-building-62c7023d79b3" rel="noopener noreferrer"&gt;Maria Rey puts it so adequately&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“This theory is learned through immersion, through building and debugging and negotiating constraints. You cannot download it; you acquire it by living with the system until the system inhabits you. This is an uncomfortable idea, because as soon as we say this, the economic picture around code trembles: the most valuable part of the program, the part that determines whether it can be safely changed, does not appear on the balance sheet. It is invisible, embodied, and therefore precarious.”&lt;/p&gt;

&lt;p&gt;– Maria Rey&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That all brought me down the track of diving into this topic, &lt;a href="https://sessionize.com/nico-krijnen/" rel="noopener noreferrer"&gt;talking about it at conferences&lt;/a&gt; and writing blogs like these about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Programming as Theory Building
&lt;/h2&gt;

&lt;p&gt;This is not some new AI-era insight either. Peter Naur, the N from BNF and a Turing Award winner for his work on Algol 60, already wrote about this in 1985 in his article “Programming as Theory Building”.&lt;/p&gt;

&lt;p&gt;Naur’s point is simple, and once you see it you cannot really unsee it. A program is not just the source code sitting in a repo. The real thing is the theory people build in their heads while working on it. Why it works that way. Why it was designed that way. What tradeoffs were made. What parts are safe to touch and what parts will bite you.&lt;/p&gt;

&lt;p&gt;What made that argument so sharp is the world he was arguing against. The dominant view back then treated programming as a kind of production process. Get the requirements in, have programmers translate them, get code and documentation out. In that picture, the human beings in the middle are mostly interchangeable. If the specs are good enough, anyone should be able to pick up the work and continue.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Input       =  Requirements &amp;amp; Observing real world
Programmers  =  Replaceable "machines"
Output      =  Code &amp;amp; Docs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftxeacqma2qmdueu35mya.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftxeacqma2qmdueu35mya.webp" alt="Comparison of the production view of programming and the theory-building view" width="720" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What Naur saw, and what I keep seeing on teams now, is that this is just not how software behaves in practice. Programs are human constructions. The important part is not only the artifact, but the understanding wrapped around it. Specs help. Docs help. Tests help. But none of them carry the full story on their own. They only really work when people still understand what problem the system is solving and why earlier decisions were made.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Input       =  Observing real world, situational experience
Programmers  =  Build a mental theory
Output      =  Theory: mapping between real world &amp;amp; code
Code        =  Side-product
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was not just philosophy for Naur. He got there by watching real projects. And that part matters, because the production view sounds reasonable right up until you have to live inside an actual system. On paper, changing software looks cheap. You edit some text. You run the build. You ship.&lt;/p&gt;

&lt;p&gt;In real teams, the expensive part is figuring out whether the change is actually safe, whether you really understood the situation, and whether you understand what side effects will occur.&lt;/p&gt;

&lt;p&gt;When he applied the Theory Building view to his observations, the actual outcomes matched exactly with what the view would predict. Software transferred from one team to another with code, docs AND human consulting resulted in success. A similar transfer took place a while later. This time only the code and docs got transferred WITHOUT human contact between the teams. The result: an unmaintainable system.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm0o16909rqvjmijy90lc.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm0o16909rqvjmijy90lc.webp" alt="Software transfer succeeds when theory moves with people and fails when only code and documents move" width="720" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The effect was not only visible in the ability to change a system, he also saw it around the ability to deal with unexpected situations. I advise you to read Naur’s original text for more details on what he described.&lt;/p&gt;

&lt;p&gt;As I already mentioned, to me this immediately clicked. Having people on the team that deeply understand a system makes a huge difference for how effective the team is going to be at adapting it and dealing with incidents. I’m sure that anyone with sufficient experience on software projects will feel the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can’t we just document the theory?
&lt;/h2&gt;

&lt;p&gt;Ok, so if that mental theory is so important, why don’t we just write it down in documentation?&lt;/p&gt;

&lt;p&gt;Seems like the obvious answer. The trouble is: &lt;strong&gt;you can’t.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Writing down your mental model of a system is the same as asking someone to write down how to ride a bike and then asking someone else who never rode a bike to learn it from the text. This is the nature of Tacit Knowledge, it is learned by doing, not by reading. When you learn cycling, you “build a feel” for the balance. Reading an essay on how gravity works is not going to help you. The same goes for someone learning the finer touches of playing a violin, or the intuition on how to react that’s built up by basket ball players.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fps2462z8fw5wja2911m1.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fps2462z8fw5wja2911m1.webp" alt="Tacit knowledge must be learned through practice and experience, while explicit knowledge can be transferred through documents and instructions" width="720" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;Tacit&lt;/strong&gt; Knowledge&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;Explicit&lt;/strong&gt; Knowledge&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Definition&lt;/td&gt;
&lt;td&gt;Personal, experiential know-how that’s hard to articulate.&lt;/td&gt;
&lt;td&gt;Formal, documented knowledge that can be written down, codified, or stored.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How it’s gained&lt;/td&gt;
&lt;td&gt;Through practice, intuition, trial-and-error, observation.&lt;/td&gt;
&lt;td&gt;Through reading, lectures, databases, manuals.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How it’s shared&lt;/td&gt;
&lt;td&gt;Mentoring, storytelling, shadowing, demonstration, practice.&lt;/td&gt;
&lt;td&gt;Books, reports, procedures, checklists, training materials.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Examples&lt;/td&gt;
&lt;td&gt;Riding a bike, a chef’s “feel” for seasoning, a leader’s intuition in a crisis.&lt;/td&gt;
&lt;td&gt;Recipes, standard operating procedures (SOPs), user manuals, financial reports.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transfer difficulty&lt;/td&gt;
&lt;td&gt;Hard to capture in words and usually requires personal interaction.&lt;/td&gt;
&lt;td&gt;Relatively easy. You can just copy, store, or transmit the written or digital record.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Value&lt;/td&gt;
&lt;td&gt;Enables creativity, innovation, and expert judgment.&lt;/td&gt;
&lt;td&gt;Provides structure, consistency, and scalability.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;strong&gt;theory of a system&lt;/strong&gt; is tacit knowledge. This is “The Art” of doing something.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;code, tests and the docs&lt;/strong&gt; are explicit knowledge. These are “the instructions” for doing something.&lt;/p&gt;

&lt;p&gt;“The Art” of doing something is &lt;strong&gt;not&lt;/strong&gt; something you can write down. It must be passed person-to-person and learnt through experience and social interaction.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“The conclusion seems inescapable that at least with certain kinds of large programs, the continued adaptation, modification, and correction of errors in them, is essentially dependent on a certain kind of knowledge possessed by a group of programmers who are closely and continuously connected with them.”&lt;/p&gt;

&lt;p&gt;– Peter Naur, Programming as Theory Building&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk9qvggg2e1ut7p49zyel.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk9qvggg2e1ut7p49zyel.webp" alt="Peter Naur" width="400" height="266"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  So what survived from the age of Naur?
&lt;/h2&gt;

&lt;p&gt;Ok, nice theory from the 80s, why doesn’t anybody seem to talk about this and why does it seem like our industry is just completely ignoring these quite fundamental insights?&lt;/p&gt;

&lt;p&gt;What I find interesting is that our industry never fully chose one side. We kept a lot of the formalist instinct, and that is not a bad thing. We like tests, contracts, static analysis, CI, typed code, all of that. At the same time, a lot of the ways good teams actually work came much closer to Naur. Pairing. Reviews. Domain conversations. Keeping developers close to incidents and operations. All of that only makes sense if human understanding is part of the product.&lt;/p&gt;

&lt;p&gt;Looking back, our industry never fully chose one side. We kept a lot of the formalist instinct, and that is not a bad thing. We like tests, contracts, static analysis, CI, typed code, all of that. This is the &lt;strong&gt;production view&lt;/strong&gt;, though less mathematical than its original advocates envisioned: an emphasis on rigorous tools and processes, from static analysis and type systems to automated testing and CI/CD pipelines.&lt;/p&gt;

&lt;p&gt;At the same time, a lot of the ways good teams actually work came much closer to Naur. Pairing. Reviews. Domain conversations. Keeping developers close to incidents and operations. These practices recognize that &lt;strong&gt;human and social factors&lt;/strong&gt; are critical. Agile, DevOps, Domain-Driven Design, sociotechnical engineering, all reflect that insight. Straight from the Agile Manifesto: "documentation is secondary to working software and team communication", Alistair Cockburn even included Naur's article in his book "Agile Software Development."&lt;/p&gt;

&lt;p&gt;So some of the thoughts of Naur made it through, but clearly not enough. After all, we're still looking at software creation as a production pipeline (we literally call it that) and we're seeing way too many places where teams operate as "feature factories".&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means in the age of AI
&lt;/h2&gt;

&lt;p&gt;Ok, so the obvious pushback is that this was the 80s and we now live in a very different world. AI can regenerate code quickly, so readable code matters less nowadays, right?!&lt;/p&gt;

&lt;p&gt;We live in a world that is under constant change. The real value of software is not preserving source files, but preserving the ability to adapt behavior when the world changes (which it will). AI helps us immensely in producing code quicker, but it does not remove the need for a human theory of why the system exists, what tradeoffs were made and what must not break.&lt;/p&gt;

&lt;p&gt;Ok, all fine, but we are doing spec-based-development now. So all these things are nicely written down in specs and if the code gets ugly, we can just regenerate the complete system from those spec, right?!&lt;/p&gt;

&lt;p&gt;Well… the trouble is that the mental theory, the “art” of the system, cannot be fully written down. Specs are incomplete artifacts too. Just getting all the relevant expert understanding from the right people into a spec is hard in the first place. And even then, they still need interpretation. Code has a strict syntax. Specs are written in natural language, and natural language is inherently ambiguous. The hardest part is usually not writing the code from the spec, but knowing which spec is wrong, stale, underspecified, politically negotiated, or missing.&lt;/p&gt;

&lt;p&gt;Rewriting a codebase every three months is only safe if the team still understands the domain and the consequences of change. AI makes syntax cheap. It does not make judgment cheap.&lt;/p&gt;

&lt;p&gt;If anything, faster code generation increases the premium on shared understanding, because a team can now produce more change than it can truly comprehend. I’m hearing some people referring to this as “cognitive debt”, and if we keep using AI the way we are doing now, that debt is going to rise at an alarming rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Knowledge walking out the door
&lt;/h2&gt;

&lt;p&gt;It is clear that the survival of human mental models in a team is way more important than we treat them in today’s industry. Where Naur warned us that programmers are not replaceable cogs in a machine, the current industry trend is to treat developers as just that: code monkeys that can be replaced with cheaper AI.&lt;/p&gt;

&lt;p&gt;At the same time, developers job-hopping every few years has been a reality for quite some time. I was curious what causes lie behind that, to help figure out what we can do about this. What I found is quite shocking.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdeya241cs4868zdy47gt.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdeya241cs4868zdy47gt.webp" alt="Cuases of Developer Turnover" width="720" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I couldn’t find any definitive source, but taking numbers from various surveys and reports the following causes jump out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Career growth / Desire for new challenges&lt;/strong&gt; To me this feels like the most logical and common reason I hear for people switching jobs. With percentages between 10 and 20, reality seems that this accounts for a fairly low percentage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Poor leadership&lt;/strong&gt; These have me rather worried. Management and poor leadership being at the same size as career growth. Clearly a topic that requires attention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Burnout / Overwork&lt;/strong&gt; The numbers for this one vary wildly, but just as the previous one. That this one is even in here is not ok.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aging / Retirement&lt;/strong&gt; Fairly low numbers here for the moment, but this is a demographic train coming our way, and no way to stop it. A lot of knowledge about the systems running today are in the heads of the more experienced engineers in our organizations. When they leave, their knowledge leaves with them, and as we already concluded, no amount of documentation is going to prevent that.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How legacy feeds itself
&lt;/h2&gt;

&lt;p&gt;The most intriguing numbers are those for &lt;strong&gt;Technical debt / Legacy&lt;/strong&gt; as reason for developer turnover. These numbers are rather high. Which, to be honest makes total sense. If a system is already hard to work with, the people on the team feel that pain every day. Every change takes longer than it should. Every incident takes too much energy to solve. And the less confidence people have in the system, the more stressful the work becomes.&lt;/p&gt;

&lt;p&gt;That is where the vicious circle kicks in. Legacy systems drive people away. When people leave, their theory of the system leaves with them. With less theory on the team, the system becomes harder to change. A harder system drives more people away. And the cycle repeats, each turn making the system a little more opaque, a little more fragile, a little more “legacy.”&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo03cqtyrbxeq7fe5uzrx.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo03cqtyrbxeq7fe5uzrx.webp" alt="Legacy as a self-reinforcing loop of stress, departures, knowledge loss and increasing system fragility" width="720" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Legacy, then, is not just technical decay. It is a self-reinforcing loop of knowledge loss. It is what remains when the system outlives the shared theory that once made it changeable.&lt;/p&gt;

&lt;p&gt;And that also explains something important. When engineers avoid touching certain parts of a system, it is usually not because they are lazy or unwilling. It is because nobody can still explain with confidence what those parts are really doing, why they were built that way, and what might break when you change them.&lt;/p&gt;

&lt;p&gt;This vicious circle is already bad enough on its own. But there are forces outside of it that are accelerating the loss of theory even further.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why keeping knowledge alive is getting harder
&lt;/h2&gt;

&lt;p&gt;That leaves me with a bigger question. With all the tools, all the process improvements, all the focus on documentation and collaboration, why does knowledge continuity seem more fragile now instead of less?&lt;/p&gt;

&lt;p&gt;I think at least part of the answer lies in two forces that are reshaping the way we build software. Remote work has removed many of the small, informal moments where tacit knowledge used to spread: overheard conversations, quick questions, spontaneous pairing, the little bits of context you absorb just by being around people who know the system well. At the same time, AI-assisted delivery lets us produce more code, faster, while reducing how much understanding humans are forced to build along the way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F39afxo3c033cg9bl5k0p.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F39afxo3c033cg9bl5k0p.webp" alt="Remote work and AI-assisted delivery eroding knowledge continuity and increasing cognitive debt" width="720" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The theory lives in human minds. Our tools increasingly let produce vast amounts of code without building understanding, what exactly are we optimizing for? Fast delivery now, unmaintainable systems in two years? Is that the bargain we are making without realizing it?&lt;/p&gt;

&lt;p&gt;Enough about the problem at hand. I’m a practical guy, so I propose we don’t let that happen. AI is a great tool, and at the same time we have some serious work ahead of us to prevent uncontrolled cognitive debt.&lt;/p&gt;

&lt;p&gt;Enough there to dig into for the next post.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>software</category>
      <category>softwaredevelopment</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>MCP Apps - Finally a UI paradigm that speaks the language of intent</title>
      <dc:creator>Nico Krijnen</dc:creator>
      <pubDate>Sat, 21 Mar 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/realworldarchitect/mcp-apps-finally-a-ui-paradigm-that-speaks-the-language-of-intent-1dkj</link>
      <guid>https://dev.to/realworldarchitect/mcp-apps-finally-a-ui-paradigm-that-speaks-the-language-of-intent-1dkj</guid>
      <description>&lt;p&gt;Earlier this month I spoke at the &lt;a href="https://aiconference.nl/" rel="noopener noreferrer"&gt;Dutch AI Conference&lt;/a&gt;. After my own talk, I attended a talk by &lt;a href="https://glennreyes.com" rel="noopener noreferrer"&gt;Glenn Reyes&lt;/a&gt; about &lt;a href="https://glennreyes.com/talks/mcp-ui-and-the-future-of-interactive-conversations" rel="noopener noreferrer"&gt;MCP-UI&lt;/a&gt;. While listening, something profound struck me, something that I think not many others may have realized. So I figured this insight is worth writing down and sharing.&lt;/p&gt;

&lt;p&gt;Glenn argued that due to the text-based nature of our interactions with LLMs and agents, we are throwing away decades of UX expertise. &lt;strong&gt;MCP-UI&lt;/strong&gt; has been an attempt to change that, allowing interactive visual components to be provided through MCP (Model Context Protocol), an open standard that gives AI assistants the ability to connect to external tools and take real action, not just generate text. Development of MCP-UI went quite fast and currently, interactive HTML-based interfaces are an official part of the &lt;strong&gt;MCP Apps&lt;/strong&gt; standard.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqneixjyhwx1taqpp7r2w.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqneixjyhwx1taqpp7r2w.webp" alt="Glenn Reyes at Dutch AI Conference 2026" width="720" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://modelcontextprotocol.io/extensions/apps" rel="noopener noreferrer"&gt;MCP Apps&lt;/a&gt; solve the &lt;em&gt;how do we make agent UIs better&lt;/em&gt; problem. But sitting in that audience, a different question grabbed me: how do you &lt;em&gt;design&lt;/em&gt; what those agent interactions should be? Because building a beautiful MCP App that solves the wrong problem is still solving the wrong problem.&lt;/p&gt;

&lt;p&gt;That question has been answered before. Not by the AI community, but by a different group of practitioners who’ve spent the past decade perfecting collaborative techniques for exactly this: understanding complex behavior, modeling intent, and building shared language between the people who know the domain and the people who write the code. These two worlds have barely met. So let’s see if we can bring these communities together!&lt;/p&gt;

&lt;h2&gt;
  
  
  Troublesome state
&lt;/h2&gt;

&lt;p&gt;UIs have always been built around &lt;em&gt;state&lt;/em&gt;. Lists, forms, search bars, toggles, modals, loading spinners, all of these require state to be managed. Many frameworks have been built in attempts to tame state complexity (Redux, MobX, signals), but the fundamental model stays the same: UI reacts to data, components reflect state. Even with these frameworks, UI code is still hard to reason about, hard to test, and far away from the language of the business.&lt;/p&gt;

&lt;h2&gt;
  
  
  Language of the business
&lt;/h2&gt;

&lt;p&gt;What do I mean with “language of the business”, and why is that important? Developers (and their coding agents) write the code. To build the right thing, they need to understand the &lt;em&gt;what&lt;/em&gt; and &lt;em&gt;why&lt;/em&gt; behind what they are building or changing. Otherwise they solve the wrong problem…&lt;/p&gt;

&lt;p&gt;The understanding that is needed typically lives with non-technical people: the users of the system or the people who know how things really work. They don’t speak your technical language and they shouldn’t have to. What you need is to build deep understanding by using a shared non-technical language. More on that later…&lt;/p&gt;

&lt;h2&gt;
  
  
  Intent
&lt;/h2&gt;

&lt;p&gt;What struck me during Glenn’s talk was that MCP-UI and MCP Apps don’t manage state in the usual way that we’ve been doing that for frontend components. Instead, the primitive is the &lt;em&gt;message&lt;/em&gt; or &lt;em&gt;tool call&lt;/em&gt;, not the component. The UI becomes a surface for expressing goals, not navigating menus. Instead of “fill form, submit, handle error state”, the interaction is “user states intent, agent invokes tool, result surfaces”.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkmegahm9jobp1pxq4n8y.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkmegahm9jobp1pxq4n8y.webp" alt="State-based UI vs intent-based interaction" width="720" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In MCP Apps, every meaningful interaction is a named action with context. Things like: “SearchProducts”, “PlaceOrder”, “SummarizeDocument”. Sounds familiar? That’s because it’s very close to the &lt;em&gt;commands&lt;/em&gt; used in EventStorming. Think of an MCP tool call like agent’s way of issuing a command to the outside world. A command is simply a request to do something. It captures what someone wants to happen in a name and with the information needed to do it. Commands model intent directly, they express “I want this to happen” rather than “set this field to that value”. They carry &lt;em&gt;meaning&lt;/em&gt;, not mechanics.&lt;/p&gt;

&lt;p&gt;A command like “ApproveExpenseReport” says &lt;em&gt;why&lt;/em&gt; something needs to happen, not just &lt;em&gt;how&lt;/em&gt; the data changes. When modeled correctly, commands speaks the language of the business, not the language of the database.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft0qkakts4qybpmrrnjm7.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft0qkakts4qybpmrrnjm7.webp" alt="MCP tool calls map to commands in domain modeling" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing Intent-based AI interactions
&lt;/h2&gt;

&lt;p&gt;Let’s start with a concrete example. Imagine you’re building an MCP App for a warehouse operations team. Order pickers need to collect items from shelves and get them to the loading dock. A picker says “plan my picks for the next batch” and the agent needs to respond with an optimized walking route through the warehouse. You can’t describe a spatial route in text. This domain &lt;em&gt;needs&lt;/em&gt; a visual interface. So how do you design it?&lt;/p&gt;

&lt;p&gt;Instead of the usual: “let the dev team dream up how this will work”, let’s do some collaborative modeling and build some deeper understanding.&lt;/p&gt;

&lt;p&gt;You get the warehouse team into the room and run an EventStorming session. You start by asking: “What are all the things that &lt;em&gt;happen&lt;/em&gt; during a pick run?” Sticky notes go up: &lt;code&gt;PickOrderReceived&lt;/code&gt;, &lt;code&gt;RouteOptimized&lt;/code&gt;, &lt;code&gt;ItemPicked&lt;/code&gt;, &lt;code&gt;CartFull&lt;/code&gt;, &lt;code&gt;BatchCompleted&lt;/code&gt;. Standard stuff, roughly what you expected.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5u1irp0m5mn371m1sftm.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5u1irp0m5mn371m1sftm.webp" alt="Warehouse pick process EventStorming board" width="720" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then the warehouse supervisor adds one: &lt;code&gt;ObstructionReported&lt;/code&gt;. You ask what that means. Turns out aisle blockages happen &lt;em&gt;every day&lt;/em&gt;: pallet spills, restocking crews. When a planned route is blocked, pickers currently find out by walking into the obstruction and rerouting on the fly, or by shouting across the floor. This was nowhere in the requirements.&lt;/p&gt;

&lt;p&gt;You keep going. &lt;code&gt;ColdChainTimerStarted&lt;/code&gt; appears: frozen items have a 12-minute window from cold storage to the insulated shipping container. &lt;code&gt;HazmatSegregationViolationDetected&lt;/code&gt;: cleaning chemicals and food products can never share a cart. The process is far richer than anyone expected. Commands pop up: &lt;code&gt;PlanPickRoute&lt;/code&gt;, &lt;code&gt;PickItem&lt;/code&gt;, &lt;code&gt;ReportObstruction&lt;/code&gt;, &lt;code&gt;SplitCart&lt;/code&gt;, &lt;code&gt;SubstituteItem&lt;/code&gt;. Each maps directly to an MCP tool definition, named in the language of the warehouse team.&lt;/p&gt;

&lt;p&gt;Then something interesting happens. When the supervisor explains how obstruction rerouting works, sticky notes aren’t enough. She grabs a marker and draws the floor plan on the whiteboard next to the event timeline. Aisles, shelving sections, cold storage tucked in the back corner, the loading dock. She marks where today’s restocking is blocking aisle 6 and draws the detour path a picker would take. Someone grabs colored markers: orange zones for hazmat storage, a blue snowflake near cold storage, red X’s for blockages.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwg01zf32a4v1oke2s7qz.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwg01zf32a4v1oke2s7qz.webp" alt="Warehouse floor plan evolving from whiteboard sketch to MCP App UI" width="720" height="255"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You zoom in on the tricky parts with Example Mapping. You pick &lt;code&gt;PlanPickRoute&lt;/code&gt; and explore the rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rule: cold chain&lt;/strong&gt; : “Given a batch that includes frozen items, when the route is planned, then cold storage items must be picked last to stay within the 12-minute window.”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rule: hazmat segregation&lt;/strong&gt; : “Given a batch with both cleaning chemicals and food products, when the route is planned, then the cart must be split and items picked in separate runs.”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge case:&lt;/strong&gt; “What if the only route to cold storage is blocked?” The supervisor pauses. “Good question. I think… the picker skips the cold items and comes back for them after the obstruction clears? We’ve never actually decided that.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1vd774kpfvniv06u4bl6.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1vd774kpfvniv06u4bl6.webp" alt="Example Mapping for PlanPickRoute MCP tool" width="720" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The sketch on the wall becomes the MCP App: a floor plan with aisles, an optimized route line, obstructions in red, color-coded item categories. The events and commands from the timeline map to tool definitions. The visual the supervisor drew to explain her world is the visual the picker will use every day. The modeling session didn’t just uncover &lt;em&gt;what&lt;/em&gt; the interactions should be, it produced the UI too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which techniques did you say?
&lt;/h2&gt;

&lt;p&gt;Understanding how things work and building systems that fit into that is something we’ve been doing for a long time already, so it’s no surprise that there are plenty of tools that we can use to do that effectively. I think the following 4 have a an exceptionally good use for designing AI agent interactions, especially when using MCP and MCP Apps.&lt;/p&gt;

&lt;h3&gt;
  
  
  EventStorming
&lt;/h3&gt;

&lt;p&gt;This collaborative modeling technique helps you understand system behavior. It maps business processes as a storyline of domain events, triggered by commands from actors. This is exactly how an AI-driven UI interaction flows.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnbhuwh2aw2f3b802ubis.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnbhuwh2aw2f3b802ubis.webp" alt="EventStorming timeline mapped to MCP interaction flow" width="720" height="189"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With &lt;a href="https://www.eventstorming.com/" rel="noopener noreferrer"&gt;EventStorming&lt;/a&gt;, the initial focus is only on events: facts that have happened. That is not by accident. When you think primarily about command and actions, it is easy to fall into the trap of modeling &lt;strong&gt;how&lt;/strong&gt; things work. They model intention: things may still end up different than intended. The big benefit of starting with events is that they force you to focus on desired outcomes, not on how you get there. That gives you the freedom to find better ways to do things, to find a simpler command trigger for an event, still arriving at the same outcome. Both EventStorming and Event Modeling also include sketching UI interactions directly on the wall: wireframes, screens, visual cues that show what the user sees at each step. For MCP Apps, those sketches naturally become the design for custom visual components.&lt;/p&gt;

&lt;h3&gt;
  
  
  Domain Storytelling
&lt;/h3&gt;

&lt;p&gt;More suitable for interview style, &lt;a href="https://domainstorytelling.org/" rel="noopener noreferrer"&gt;domain storytelling&lt;/a&gt; captures how actors work together to achieve goals through concrete work stories. These stories help you understand how the domain actually works. Again, this is a very natural fit for mapping the intent-flows of AI agent and MCP interactions, told in the language of the domain. When you use this technique, you draw an easy to understand visual story of what the person you’re talking to is explaining. The key here is the quick feedback and validation. If you misunderstood something, they will instantly point out the mistake in your drawing or point out edge cases not yet covered.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fib1s53e4bfqx45b43pn1.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fib1s53e4bfqx45b43pn1.webp" alt="Domain Storytelling" width="720" height="168"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Event Modeling
&lt;/h3&gt;

&lt;p&gt;Very similar to EventStorming, also modeling the intent (commands), the facts (events) and the views (read models). This feels like an almost perfect conceptual match for MCP tool calls, conversation history and context windows.&lt;a href="https://eventmodeling.org/" rel="noopener noreferrer"&gt;Event Modeling&lt;/a&gt; organizes parts of the flow as vertical &lt;em&gt;slices&lt;/em&gt;, each slice a complete flow from intent to state change to updated view. The key being that each slice is an isolated piece of behavior that can be implemented as one unit of work. This makes estimation and planning a breeze. As each slice is isolated, it can also be implemented in parallel with other slices without waiting for the rest of the system to be done. Productivity-wise, this is rather interesting now that we can put many AI agents to work in parallel to each build a piece of a system. Slices also map nicely to how MCP Apps work, each slice essentially one tool interaction: what the user asks for, what happens and what they see as a result.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fntdup4rahweaoim4dxrg.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fntdup4rahweaoim4dxrg.webp" alt="Event Modeling" width="720" height="286"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Example Mapping
&lt;/h3&gt;

&lt;p&gt;The more concrete you can make it, the easier it is to spot edge cases or incorrect assumption. &lt;a href="https://cucumber.io/blog/bdd/example-mapping-introduction/" rel="noopener noreferrer"&gt;Example Mapping&lt;/a&gt; is an excellent tool to quickly explore the rules around a specific command or intent: given &lt;em&gt;this&lt;/em&gt; situation, when &lt;em&gt;this&lt;/em&gt; happens, then &lt;em&gt;this&lt;/em&gt; should be the outcome. Perfect to capture exactly what an MCP tool should do and how it should respond in different scenarios.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0zu7h6t80bbnvpkwd31k.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0zu7h6t80bbnvpkwd31k.webp" alt="Example Mapping" width="720" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Nowadays, you can take the raw output of an example mapping session, give it to your coding assistant and turn it into a test-suite. And with good behavioral tests and the right architecture guidelines, your coding agent will have a breeze creating the implementation for you too.&lt;/p&gt;




&lt;p&gt;What I find most interesting is how all of these techniques are not just similar to how MCP tools work, conceptually they map almost one-to-one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real superpower
&lt;/h2&gt;

&lt;p&gt;All these are not just modeling tools, they are &lt;strong&gt;collaborative discovery&lt;/strong&gt; tools. They work so well at uncovering complex behavior because they put &lt;strong&gt;experts and developers in the same room&lt;/strong&gt; : working side by side without any intermediaries. This direct contact creates shared understanding &lt;em&gt;before&lt;/em&gt; a single line of code is written. It also creates that insight very quickly and much deeper than with written specs. Result: high focus during implementation, with very few questions or points of confusion. And when the inevitable unforeseen edge case emerges, the personal connections that developers have built up with the experts mean that they know exactly who to call.&lt;/p&gt;

&lt;p&gt;Contrast that with the traditional approach: &lt;strong&gt;BA → Specs → PO → Story → Tasks → Dev/AI → Code&lt;/strong&gt;. Where every handoff is a translation loss: the telephone-game effect in action. By the time the requirements reach the implementor, they are abstract, ambiguous, full of assumptions and often wrong in ways nobody notices until late. The typical result is expensive rework, long feedback cycles and clarity arriving at the worst possible moment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa0iyalcnuf49slsxrqoa.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa0iyalcnuf49slsxrqoa.webp" alt="Translation loss through handoffs vs direct collaborative discovery" width="720" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The language aspect matters even more for our interaction with AI agents, where the “UI” is largely &lt;em&gt;language&lt;/em&gt;. If you get the intent vocabulary wrong, the whole thing feels broken to it’s users. This focus on precise, shared language has its roots in &lt;a href="https://www.domainlanguage.com/ddd/" rel="noopener noreferrer"&gt;Domain-Driven Design&lt;/a&gt; and is now more important than ever. Getting the vocabulary right is no longer just a modeling concern, it’s a usability concern. I guess that deserves a blog of its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this all means for you
&lt;/h2&gt;

&lt;p&gt;AI agents are becoming part of how we build, design and use software. Intent-based interaction through MCP fit naturally to command and event based design tools. Collaborative modeling techniques like EventStorming, Domain Storytelling, Event Modeling, and Example Mapping have been perfected over the past decade for exactly this kind of problem: understanding what a system should do by working directly with the people who know. If you want to build MCP Apps that actually solve the right problems, &lt;strong&gt;start by getting the right people in the room&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you’d like a hand getting started, whether it’s facilitating a good modeling session or designing MCP tool interactions that speak your domain’s language, &lt;a href="https://realworldarchitect.dev/contact/" rel="noopener noreferrer"&gt;reach out&lt;/a&gt;. I’d love to help.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>mcp</category>
      <category>ui</category>
    </item>
    <item>
      <title>Traceable Tests</title>
      <dc:creator>Nico Krijnen</dc:creator>
      <pubDate>Fri, 27 Jan 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/realworldarchitect/traceable-tests-b6c</link>
      <guid>https://dev.to/realworldarchitect/traceable-tests-b6c</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.luminis.eu/blog/traceable-tests/" rel="noopener noreferrer"&gt;Luminis&lt;/a&gt; on 27 Jan 2023.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Have you ever spent hours or days trying to figure out why some API test is failing? Whenever something like that happens to me, my immediate thought is: what could have helped me find this problem faster?&lt;/p&gt;

&lt;p&gt;Unit tests are much better at this, when they fail, you just run them again locally, set a breakpoint if you have to, and find out what failed. But with tests that run against your API (yes, you need those too!) it can be much harder to figure out exactly what code got executed. There are all sorts of boundaries that make this hard, especially when those tests run as part of your pipeline (yes, you need to do that!). You can’t just set a breakpoint in your pipeline and even if you have logging in your backend, how do you know which log entries resulted from which test scenario?&lt;/p&gt;

&lt;p&gt;I’ve been playing with the idea of linking backend logs to tests for some time. Years back, I had a plan to add the name of the test to a custom HTTP header when calling the API from tests. In the backend I could add that value to the MDC context that we attach to log entries. The result would be that for each log entry, you could see which test triggered it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Déjà vu
&lt;/h2&gt;

&lt;p&gt;Somehow I never got around to implementing that idea. Last year though, I helped out a team working on an API-only service that ran into the very same problem. Their system is comprised of a set of microservices and some of the API calls go through several of these services to handle the request. Next to unit tests, one of their other automated validations is a regression test suite that runs against the API to validate expected behavior.&lt;/p&gt;

&lt;p&gt;So far, so good, and that test suite was really valuable. It allowed the team to quickly implement new functionality without spending a lot of time checking that existing functionality kept working.&lt;/p&gt;

&lt;p&gt;You know what’s coming now… whenever a test would fail, it was a nightmare to figure out why.&lt;/p&gt;

&lt;p&gt;Luckily, I had just finished hooking up all the services to an &lt;a href="https://www.elastic.co/cloud/" rel="noopener noreferrer"&gt;Elastic Cloud&lt;/a&gt; environment, with &lt;a href="https://www.elastic.co/observability/application-performance-monitoring" rel="noopener noreferrer"&gt;APM-powered tracing&lt;/a&gt; and all the good stuff. The traces gave excellent insight into what happened inside and between the microservices. However, it was still really hard to link a specific test failure to a set of traces.&lt;/p&gt;

&lt;h2&gt;
  
  
  OpenTelemetry FTW
&lt;/h2&gt;

&lt;p&gt;I still had that idea from before in my head, and when I saw the team struggling with these failing tests, a new idea popped in: what if I linked the traces to tests?&lt;/p&gt;

&lt;p&gt;The typical method to pass trace IDs across service calls is by using a trace header. OpenTelemetry provides the &lt;code&gt;traceparent&lt;/code&gt; header for that, and the &lt;a href="https://www.elastic.co/guide/en/apm/agent/index.html" rel="noopener noreferrer"&gt;Elastic APM agent&lt;/a&gt; being used in the services &lt;a href="https://www.elastic.co/guide/en/apm/agent/java/current/supported-technologies-details.html" rel="noopener noreferrer"&gt;picks that up out-of-the-box&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;My first idea was to return the &lt;code&gt;traceparent&lt;/code&gt; header in the API responses and then log them with each test. However, that’s not really how the OpenTelemetry &lt;code&gt;traceparent&lt;/code&gt; header is supposed to be used. You’re supposed to include the header in the calling request. So that’s what I tried next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Show me the code!
&lt;/h2&gt;

&lt;p&gt;First thing I did was locate how the test suite was making the API calls. Turned out this was done with &lt;code&gt;HttpClient&lt;/code&gt;, which offers a nice way to inject additional headers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;TraceInstrumentationInit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;openTelemetry&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;TraceInstrumentationInit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tracer&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HttpClientTraceInstrumentation&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;HttpProcessor&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="nd"&gt;@Override&lt;/span&gt;
 &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;HttpContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="n"&gt;injectTraceHeaders&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;

 &lt;span class="nd"&gt;@Override&lt;/span&gt;
 &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpResponse&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;HttpContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;

 &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;injectTraceHeaders&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="c1"&gt;// TODO&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;

 &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;HttpClient&lt;/span&gt; &lt;span class="nf"&gt;registerTraceInstrumentation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
 &lt;span class="nc"&gt;DefaultHttpClient&lt;/span&gt; &lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="nc"&gt;HttpClientTraceInstrumentation&lt;/span&gt; &lt;span class="n"&gt;traceInstrumentation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
 &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpClientTraceInstrumentation&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addRequestInterceptor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;traceInstrumentation&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
 &lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addResponseInterceptor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;traceInstrumentation&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
 &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ok, so that gives us a way to inject the &lt;code&gt;traceparent&lt;/code&gt; header. First I figured I could generate the trace ID myself. However, it turns out &lt;a href="https://www.w3.org/TR/trace-context/#traceparent-header-field-values" rel="noopener noreferrer"&gt;the &lt;code&gt;traceparent&lt;/code&gt; value&lt;/a&gt; is not just a trivial UUID, it has a very specific structure and I wouldn’t advise trying to generate it yourself.&lt;/p&gt;

&lt;p&gt;Luckily, OpenTelemetry has excellent SDKs that make it easy to generate and inject these headers. So we need a dependency on the OpenTelemetry API and SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependencyManagement&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;dependencies&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;io.opentelemetry&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;opentelemetry-bom&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;1.18.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;type&amp;gt;&lt;/span&gt;pom&lt;span class="nt"&gt;&amp;lt;/type&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;scope&amp;gt;&lt;/span&gt;import&lt;span class="nt"&gt;&amp;lt;/scope&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;/dependencies&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependencyManagement&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;dependencies&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;io.opentelemetry&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;opentelemetry-api&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;io.opentelemetry&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;opentelemetry-sdk&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependencies&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we had to initiate a trace for each scenario, and a sub-span for each API call being made.&lt;/p&gt;

&lt;p&gt;After looking at the codebase, we saw we could add that to an already present base class used by all tests to report scenario results to &lt;a href="https://www.gurock.com/testrail/" rel="noopener noreferrer"&gt;TestRail&lt;/a&gt;. Personally I’m not a big fan of using base classes for tests as it can be hard to maintain a sensible hierarchy and the use of inheritance creates tight coupling between tests, which we don’t want. In JUnit for example, I would use &lt;code&gt;@ExtendWith&lt;/code&gt; to keep such responsibilities separated and easier to re-use and to avoid the use of static instances as seen here. But we didn’t want to overhaul the complete test suite just to introduce this, better not to mix a big refactoring like that. The main goal at this point was to validate that traces could work.&lt;/p&gt;

&lt;p&gt;As you see, we also passed the &lt;code&gt;traceId&lt;/code&gt; to TestRail, more on that later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Hooks&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;BaseSteps&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;TestRailReporter&lt;/span&gt; &lt;span class="n"&gt;testRailReporter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
 &lt;span class="nc"&gt;TestRailReporterFactory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ScenarioTraceInstrumentation&lt;/span&gt; &lt;span class="n"&gt;instrumentation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
 &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ScenarioTraceInstrumentation&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

 &lt;span class="nd"&gt;@Before&lt;/span&gt;
 &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Scenario&lt;/span&gt; &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="n"&gt;instrumentation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;startTrace&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;

 &lt;span class="nd"&gt;@After&lt;/span&gt;
 &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;teardown&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Scenario&lt;/span&gt; &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;traceId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;instrumentation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;endTrace&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="n"&gt;testRailReporter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reportResultToTestRail&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;traceId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For every scenario we start a new span, including attributes to link it to the scenario. We also do some rudimentary logging for every scenario, making it easy to spot in the logs which scenario ran at which point and what the trace ID was for that scenario run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;TraceInstrumentationInit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tracer&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ScenarioTraceInstrumentation&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ThreadLocalSpan&lt;/span&gt; &lt;span class="n"&gt;spanState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ThreadLocalSpan&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

 &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;startTrace&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Scenario&lt;/span&gt; &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="nc"&gt;Span&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spanState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
 &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;spanBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
 &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setSpanKind&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SpanKind&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;CLIENT&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
 &lt;span class="o"&gt;);&lt;/span&gt;
 &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAttribute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"test.scenario_id"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

 &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"|---------\n"&lt;/span&gt;
 &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"| SCENARIO: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;" - "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"\n"&lt;/span&gt;
 &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"| trace.id: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSpanContext&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getTraceId&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;

 &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;endTrace&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;traceId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spanState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;traceId&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="n"&gt;spanState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;traceId&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To keep track of span state, I used a &lt;code&gt;ThreadLocal&lt;/code&gt;, exposing a simple API to start and end the span that we could use from the &lt;code&gt;ScenarioTraceInstrumentation&lt;/code&gt; and &lt;code&gt;HttpClientTraceInstrumentation&lt;/code&gt;. As you see I did get lazy a bit here, using a &lt;code&gt;Pair&lt;/code&gt; to store both the &lt;code&gt;Span&lt;/code&gt; and &lt;code&gt;Scope&lt;/code&gt;, using a record for that would make the code easier to read.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ThreadLocalSpan&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ThreadLocal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Pair&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Span&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Scope&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ThreadLocal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;

 &lt;span class="nc"&gt;Span&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SpanBuilder&lt;/span&gt; &lt;span class="n"&gt;spanBuilder&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="nc"&gt;Span&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spanBuilder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;startSpan&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="nc"&gt;Scope&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;makeCurrent&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Pair&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
 &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;

 &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;traceId&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getLeft&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getSpanContext&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getTraceId&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;

 &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="nc"&gt;Pair&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Span&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Scope&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pair&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="n"&gt;pair&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRight&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;close&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="n"&gt;pair&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLeft&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The OpenTelemetry SDK also needs some initialization. Keeping it simple for this first experiment, and since some of the other code was static already, we just created two package scoped statics to be used from the &lt;code&gt;ScenarioTraceInstrumentation&lt;/code&gt; and &lt;code&gt;HttpClientTraceInstrumentation&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TraceInstrumentationInit&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;OpenTelemetry&lt;/span&gt; &lt;span class="n"&gt;openTelemetry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;initOpenTelemetry&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Tracer&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openTelemetry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTracer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"regressionTest"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

 &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;OpenTelemetry&lt;/span&gt; &lt;span class="nf"&gt;initOpenTelemetry&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="nc"&gt;SdkTracerProvider&lt;/span&gt; &lt;span class="n"&gt;sdkTracerProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SdkTracerProvider&lt;/span&gt;
 &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="nc"&gt;OpenTelemetrySdk&lt;/span&gt; &lt;span class="n"&gt;sdk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenTelemetrySdk&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
 &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setTracerProvider&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sdkTracerProvider&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
 &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setPropagators&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ContextPropagators&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
 &lt;span class="nc"&gt;W3CTraceContextPropagator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
 &lt;span class="o"&gt;))&lt;/span&gt;
 &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="nc"&gt;Runtime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRuntime&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;addShutdownHook&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
 &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;sdkTracerProvider:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
 &lt;span class="o"&gt;);&lt;/span&gt;
 &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sdk&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And finally, we could put all the pieces together in the &lt;code&gt;HttpClientTraceInstrumentation&lt;/code&gt;, see below.&lt;/p&gt;

&lt;p&gt;We create an additional span for the request [1] and then inject the trace header into the request [2].&lt;/p&gt;

&lt;p&gt;The OpenTelemetry SDK code is very generic, allowing you to inject headers into pretty much anything, from HTTP requests to messages with all sorts of client libraries. The extremely generic code does make it a bit hard to read, but essentially you need to pass it a method that accepts two string arguments, the header name and header value. The &lt;code&gt;HttpRequest&lt;/code&gt; from &lt;code&gt;HttpClient&lt;/code&gt; has exactly such a method: &lt;code&gt;setHeader&lt;/code&gt; [3].&lt;/p&gt;

&lt;p&gt;Last but not least, we also log every request being made [4], giving a nice overview in the test logs of what API calls are being made for each scenario.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;TraceInstrumentationInit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;openTelemetry&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;TraceInstrumentationInit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tracer&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HttpClientTraceInstrumentation&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;HttpProcessor&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="no"&gt;W3C_TRACEPARENT_HEADER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Traceparent"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

 &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;TextMapPropagator&lt;/span&gt; &lt;span class="n"&gt;textMapPropagator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
 &lt;span class="n"&gt;openTelemetry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPropagators&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getTextMapPropagator&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

 &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;TextMapSetter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;HttpRequest&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;setter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
 &lt;span class="nl"&gt;HttpRequest:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;setHeader&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// [3] http client setHeader method&lt;/span&gt;

 &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ThreadLocalSpan&lt;/span&gt; &lt;span class="n"&gt;spanState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ThreadLocalSpan&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

 &lt;span class="nd"&gt;@Override&lt;/span&gt;
 &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;HttpContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="nc"&gt;Span&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spanState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
 &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;spanBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
 &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setSpanKind&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SpanKind&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;CLIENT&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
 &lt;span class="o"&gt;);&lt;/span&gt;
 &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Scope&lt;/span&gt; &lt;span class="n"&gt;ignored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;makeCurrent&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="c1"&gt;// [1] add span details&lt;/span&gt;
 &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAttribute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;HTTP_METHOD&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRequestLine&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getMethod&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
 &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAttribute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;HTTP_URL&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRequestLine&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getUri&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
 &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAttribute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"component"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"http"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

 &lt;span class="n"&gt;injectTraceHeader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
 &lt;span class="n"&gt;logRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;

 &lt;span class="nd"&gt;@Override&lt;/span&gt;
 &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpResponse&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;HttpContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="n"&gt;spanState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;

 &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;injectTraceHeader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="c1"&gt;// [2] let opentelemetry sdk propagate any required headers&lt;/span&gt;
 &lt;span class="n"&gt;textMapPropagator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;current&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;setter&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;

 &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;logRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="c1"&gt;// [4]&lt;/span&gt;
 &lt;span class="s"&gt;"|- "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRequestLine&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"\n"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
 &lt;span class="s"&gt;"| ^- "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLastHeader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;W3C_TRACEPARENT_HEADER&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Everything coming together
&lt;/h2&gt;

&lt;p&gt;Now when tests are run, their logs clearly show each of the scenarios, their trace ID and the API calls being invoked. And for every API call, the complete &lt;code&gt;traceparent&lt;/code&gt; header is shown. With these details being propagated to our backend, we can now look up all the calls for a scenario in Elastic, or even search there for the span ID to locate the trace for a specific API call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;|---------
| SCENARIO: T2748705 - Validate if method PUT was added to controller and it's possible to use it
| trace.id: d28a64332015de5d324cb3e0f0380eba
|- PUT http://.../api/...
| ^- traceparent: ...
...
...
|---------
| SCENARIO: ...
| trace.id: ...
|- GET http://.../api/...
| ^- traceparent: ...
...
...

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Integrate existing tools
&lt;/h2&gt;

&lt;p&gt;At this client, the team used &lt;a href="https://www.gurock.com/testrail/" rel="noopener noreferrer"&gt;TestRail&lt;/a&gt; to centrally view reports of all test runs. To find the cause for a failing test, wouldn’t it be nice if we could skip sifting through logs? As you saw before, we had the trace ID available when we posted results to TestRail, so we added a link there to take you directly to the corresponding trace in Elastic.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc07hoomzdn9w73bjvygt.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc07hoomzdn9w73bjvygt.webp" alt="Link to Elastic trace integrated into TestRail" width="720" height="718"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Results!
&lt;/h2&gt;

&lt;p&gt;When you follow that link to Elastic, it shows you all the API calls (transactions) that were part of that test scenario (trace).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcm3dqij5pyi9ghlqni72.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcm3dqij5pyi9ghlqni72.webp" alt="Trace in Elastic" width="720" height="317"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From there you can navigate to each API call and look at individual traces. Elastic shows a lot of useful info, like a latency distribution chart that makes it super easy to quickly select all slow calls or all failing calls.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyx1mre8z78gu9oxwb8vj.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyx1mre8z78gu9oxwb8vj.webp" alt="Trace details in Elastic" width="720" height="293"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is also a dedicated errors section, which allows you to quickly locate details for any errors that occurred as part of the scenario traces.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg71ammjzh5akiyot3lvq.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg71ammjzh5akiyot3lvq.webp" alt="Trace errors in Elastic" width="720" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tip: If you want to take a closer look at what’s in these kind of traces, there is a nice &lt;a href="https://www.elastic.co/demos" rel="noopener noreferrer"&gt;free Elastic demo environment&lt;/a&gt; where you can play around with traces in the APM section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;With just a little bit of code, we managed to reduce the amount of time needed to pinpoint test failures dramatically. Which shows again that optimizing for quick work loops and short feedback cycles are crucial for improving software delivery performance.&lt;/p&gt;

</description>
      <category>api</category>
      <category>backend</category>
      <category>productivity</category>
      <category>testing</category>
    </item>
    <item>
      <title>Move Faster with New Feature Flag Capabilities in AWS</title>
      <dc:creator>Nico Krijnen</dc:creator>
      <pubDate>Mon, 13 Dec 2021 00:00:00 +0000</pubDate>
      <link>https://dev.to/realworldarchitect/move-faster-with-new-feature-flag-capabilities-in-aws-37cj</link>
      <guid>https://dev.to/realworldarchitect/move-faster-with-new-feature-flag-capabilities-in-aws-37cj</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.luminis.eu/blog/move-faster-with-new-feature-flag-capabilities-in-aws/" rel="noopener noreferrer"&gt;Luminis&lt;/a&gt; on 13 Dec 2021.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I always get excited about things that allow you to bring new ideas to production faster. And it seems we are in luck! During the yearly AWS re:Invent conference that took place in Las Vegas, AWS launched several services that help you do just that!&lt;/p&gt;

&lt;p&gt;Business around us is changing more and more rapidly. Year by year, the State of Devops research confirms that high performing companies excel in adapting to those changes by optimizing their ability to experiment, deliver quickly and gather feedback directly. You need to try many ideas and quickly find out which are successful and which are not. That ability to make many small changes reduces risk and increases the ability to innovate.&lt;/p&gt;

&lt;p&gt;The new AWS services around feature flags enable organizations to achieve these goals. I will explain these briefly and show you how you can leverage them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving fast
&lt;/h2&gt;

&lt;p&gt;So what ingredients do you need to go fast? First, you have to optimize your organization and teams &lt;a href="https://www.linkedin.com/posts/nicokrijnen_adopting-an-experimental-mindset-mark-rickmeier-activity-6876081797009022976-S-XA" rel="noopener noreferrer"&gt;so they can come up with diverse ideas&lt;/a&gt;. Make sure you enable them to try those out and get feedback quickly without having to go through committees to get approval.&lt;/p&gt;

&lt;p&gt;Second, on the engineering side, you need to learn to &lt;a href="https://www.linkedin.com/posts/nicokrijnen_how-the-right-architecture-can-simplify-migrating-activity-6871732324489277440-pe35" rel="noopener noreferrer"&gt;do everything in small steps&lt;/a&gt;. Integrate all your changes with the rest of your team often, at least daily. This way of working is called trunk-based development. The key here is never letting go of high quality and keeping your software in a constant state of being able to deploy it to production. To achieve that, you need a (continuous) delivery pipeline that automates all the steps needed to get from commit to deploy. As &lt;a href="https://youtu.be/x9l6yw1PFbs?t=851" rel="noopener noreferrer"&gt;excellently explained by Dave Farley&lt;/a&gt;, the key responsibility of that pipeline is not to ensure that the software is 100% correct – that is not realistic. Instead, the pipeline needs to do all that it can to figure out if something might be broken or misbehaving. That means you’ll need at least a solid automated test suite that provides you with confidence that on the functional side everything works. Next to that, you can add validations to your pipeline for things like performance, code quality, and any other aspects that are important for your application.&lt;/p&gt;

&lt;p&gt;If you do everything in small steps, does that mean you cannot have big innovations? No, of course it doesn’t. Big innovations never happen in one go. They are the combination of smaller increments, even the failed ones that you learned from. Like Edison’s light bulb, which was the result of thousands of failed experiments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback, as early as possible
&lt;/h2&gt;

&lt;p&gt;A common trick is to separate the deployment of changes from the release of a feature. By using feature flags, you can include the new code you are writing without the feature showing up or being active. With all the code being deployed continuously, the act of “releasing” a feature simply becomes toggling the feature flag.&lt;/p&gt;

&lt;p&gt;This way of working helps greatly in catching mistakes or wrong assumptions in your ideas early, while it is still easy to try another approach. It is much better to discover that your idea does not work after a few hours than finding out weeks later, when you’ve already invested a lot of time. You also make it much easier to experiment with new features much earlier in their development, maybe just exposing them to a few interested beta users and receive feedback before you make the feature generally available.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjk2n0ukl84g7irkxvcvy.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjk2n0ukl84g7irkxvcvy.webp" width="720" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter feature flags
&lt;/h2&gt;

&lt;p&gt;I always like keeping things simple. The easiest way to implement feature flags is to just add some booleans and ‘if statements’ in your code and allow these to be toggled between the different environments you have. For example, showing the new feature you are working on in your staging environment, but hiding and deactivating it in production. To be honest, in many cases, a simple mechanism like this is more than sufficient.&lt;/p&gt;

&lt;p&gt;Do realize there is a tradeoff when using feature flags. Having these branches in your code means you add complexity and you make your application less predictable. In many cases the benefits greatly outweigh the downside, as long as you keep it under control. Don’t let hundreds of feature flags linger around. Be responsible, be a good craftsman and keep your codebase clean. Once a feature is released, clean up all traces of the flag. That way you minimize complexity and keep your codebase lean and mean and easy to change. After all, you want to keep doing more experiments and keep delivering value in a sustainable way.&lt;/p&gt;

&lt;p&gt;Depending on how long your build pipeline takes, having to change booleans in code may mean that it takes a while to toggle a feature in production. This may be ok in some cases, but when it turns out that a feature doesn’t play nice on production yet, you want to toggle it off again quickly. What if you could toggle it in real-time and let someone else handle part of the complexity around feature flags?&lt;/p&gt;

&lt;h2&gt;
  
  
  Launch 1: AWS AppConfig – Feature Flags
&lt;/h2&gt;

&lt;p&gt;AWS AppConfig has been around since 2019 and allows you to roll out configuration changes to your applications in real-time, without having to deploy code or take your application out of service. It can validate and then roll out configuration changes gradually while monitoring your application and rolling back changes in case of errors, minimizing impact to users.&lt;/p&gt;

&lt;p&gt;Recently AWS launched AWS AppConfig Feature Flags, which builds on AppConfig and makes it easier to roll out new configurations of your feature toggles. You can now setup and toggle feature flags directly from the AWS Console. Versions of your feature flags configuration are tracked and as with config changes, you can manage and monitor the gradual deployment to your environments.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpjriw4p6dypodfnot2fj.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpjriw4p6dypodfnot2fj.webp" alt="Feature flags including attributes in AWS AppConfig" width="720" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As feature flags are just booleans, there is no validation around them. You can also add custom attributes to flags. An example of what I would use these for is creating flags that are only enabled for specific beta tenants. For these attributes you can set up some simple validation rules in the UI. That may not be as powerful as writing a validation lambda as you can do for normal AppConfig validation, but it sure is a lot simpler and probably sufficient. And don’t forget, less handcrafted code means you reduce risk.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7iw2xvjgq119m126o4u2.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7iw2xvjgq119m126o4u2.webp" alt="Feature flag attributes in AWS AppConfig" width="720" height="215"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Apart from feature flags, you can also use this feature to toggle operational aspects of your application quickly. For example, in case of an incident you can temporarily switch off non-critical functionality or behavior to reduce load and keep the essentials of your service working, allowing your team to focus on resolving the incident.&lt;/p&gt;

&lt;p&gt;That all sounds fantastic, but it does mean that releases of features are no longer tracked in your central git history, instead they are changed through the AWS Console. Luckily, the Console does track versions of your configurations and shows you the history of feature flags and configuration deployments that have been done to each of your environments. So, it may not be in one place anymore, but you can still audit when features went live or were rolled back. Just be aware that you are making this tradeoff.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs047g650iw0cdqzpttk4.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs047g650iw0cdqzpttk4.webp" alt="AWS AppConfig deployment history" width="720" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Launch 2: AWS CloudWatch – Evidently
&lt;/h2&gt;

&lt;p&gt;Where AppConfig Feature Flags is geared towards simplifying the technicality of managing feature flags and their deployment, Evidently is all about connecting the data you collect for experiments and A/B tests around feature roll outs that you do with smaller sets of users before making the feature generally available.&lt;/p&gt;

&lt;p&gt;Evidently allows you to set up multiple variants of a feature and test these variants on different subsets of users at the same time. Once you have set up and are running such an experiment, Evidently also helps in statistically analyzing which variant is performing better, based on data collected during the experiment, giving you confidence in making the right choices.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8cu20dquuogkwgnwk4vp.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8cu20dquuogkwgnwk4vp.webp" alt="AWS CloudWatch Evidently getting started with experiments" width="720" height="265"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In today’s fast paced world, you have to continuously reinvent your business to remain relevant. To do so you have to innovate and Evidently really helps you with tools to apply that mindset of running experiments and basing your decisions on the data that you gather. In today’s fast paced world, you have to continuously reinvent your business to remain relevant. To do so you have to innovate and Evidently really helps you with tools to apply that mindset of running experiments and basing your decisions on the data that you gather.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7sh4ccj6mhxm36ppv3rp.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7sh4ccj6mhxm36ppv3rp.webp" alt="AWS CloudWatch Evidently audience distribution" width="720" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Launch 3: Amazon CloudWatch RUM (Real User Monitoring)
&lt;/h2&gt;

&lt;p&gt;Now obviously, to make a service like Evidently work well, you need to collect the right data. Data that helps you prove the effectiveness and desired behavior of the features you build. To help with this, AWS launched RUM, another service under the CloudWatch umbrella.&lt;/p&gt;

&lt;p&gt;CloudWatch is all about monitoring, but so far that was mostly oriented towards raw infrastructure and backend metrics of apps running inside AWS. RUM enhances that with capabilities to monitor web application performance, aimed at user experience. You inject some JavaScript into your web application frontend which then allows you to anonymously collect page load and layout performance metrics, JavaScript errors and client-side experienced HTTP errors.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F21aawho10k8pci1c69wl.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F21aawho10k8pci1c69wl.webp" alt="AWS CloudWatch RUM getting started" width="720" height="252"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Where traditional CloudWatch metrics are somewhat raw, RUM feels more like Google Analytics, giving you out-of-the-box dashboards that provide visibility into your application’s reliability, performance, and end user satisfaction. The service performs analysis on the collected data and breaks it down so you can gain insights about which areas of your application’s user experience can be improved.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbge8hyj48ko7hw7x7woc.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbge8hyj48ko7hw7x7woc.webp" alt="AWS RUM average load times" width="720" height="379"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the observability side, CloudWatch RUM integrates with AWS X-Ray, so you can view traces and segments for end user requests. You can even see your clients in CloudWatch ServiceLens service maps.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fojci13hnmjvnutbbmhpy.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fojci13hnmjvnutbbmhpy.webp" alt="AWS RUM tracing with AWS X-Ray" width="720" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;p&gt;As usual with AWS, pricing for these services are based on usage, which means you can try these out on a small scale and then evaluate if the benefits they bring you are worth the cost. Once you use these services in production, always put up some cost monitoring so you remain aware of what you are spending. Below you will find an overview of the pricing for these services. For details, look at their pricing page.&lt;/p&gt;

&lt;p&gt;AWS AppConfig Feature Flags are charged by the number of times you request and receive config changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;$0.0000002 per configuration request via API Calls&lt;/li&gt;
&lt;li&gt;$0.0008 per configuration received&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AWS CloudWatch Evidently has a first time free trial that includes 3 million Evidently events and 10 million Evidently analysis units per account. After that you pay:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;$5 per 1 million events (user actions &amp;amp; assignment events)&lt;/li&gt;
&lt;li&gt;$7.50 per 1 million analysis units&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AWS CloudWatch RUM allows you to control costs by configuring it to only analyze a limited percentage of user sessions. There is a first time free trial that includes 1 million RUM events per account. After that you pay:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;$1 for every 100,000 events collected (page view, JavaScript error, HTTP error)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Alternatives
&lt;/h2&gt;

&lt;p&gt;Compared to products that specialize in this field like LaunchDarkly or Optimizely, these AWS services may feel somewhat bare bones. This is no coincidence, as Werner Vogels said: AWS services are designed to be building blocks, not frameworks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmn2p4q9z7tfs08aqd0ac.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmn2p4q9z7tfs08aqd0ac.webp" alt="LaunchDarkly showing current state of feature flag in code editor" width="720" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AWS may not be as feature rich as some of these others but having it all integrated into the rest of your service stack can be a big benefit. The low barrier to entry means you can start using it today and get value out of it quickly, or switch to something else later if you discover that your needs have grown.&lt;/p&gt;

&lt;p&gt;And don’t forget, if you don’t need all the experimentation statistics or real-time flag controls, just keep it simple and add a few booleans in your code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;It is great to see that AWS is serious about giving their users the keys they need for innovation. What I personally find most exciting is that with this combination of services, AWS makes it easier for all of us to adopt that mindset of doing experiment-based and data-driven roll outs.&lt;/p&gt;

&lt;p&gt;If you need help to introduce that mindset of experimentation, don’t hesitate to reach out!&lt;/p&gt;

&lt;h2&gt;
  
  
  Learn more
&lt;/h2&gt;

&lt;p&gt;If you want to learn more about these AWS services, check out the official blogs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/mt/introducing-aws-appconfig-feature-flags-in-preview/" rel="noopener noreferrer"&gt;Introducing AWS AppConfig Feature Flags In Preview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/aws/cloudwatch-evidently/" rel="noopener noreferrer"&gt;New – Amazon CloudWatch Evidently – Experiments and Feature Management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/aws/cloudwatch-rum/" rel="noopener noreferrer"&gt;New – Real-User Monitoring for Amazon CloudWatch&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>news</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
