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

<channel>
	<title>ShiftMag</title>
	<atom:link href="https://shiftmag.dev/feed/" rel="self" type="application/rss+xml" />
	<link>https://shiftmag.dev/</link>
	<description>Insightful engineering content &#38; community</description>
	<lastBuildDate>Fri, 30 Oct 2026 12:35:53 +0000</lastBuildDate>
	<language>en-GB</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.3</generator>

<image>
	<url>https://shiftmag.dev/wp-content/uploads/2026/09/cropped-Shift-32x32.webp</url>
	<title>ShiftMag</title>
	<link>https://shiftmag.dev/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>You shouldn&#8217;t trust AI agents in production</title>
		<link>https://shiftmag.dev/you-shouldnt-trust-ai-agents-in-production-13232/</link>
		
		<dc:creator><![CDATA[Marko Crnjanski]]></dc:creator>
		<pubDate>Fri, 30 Oct 2026 12:35:53 +0000</pubDate>
				<category><![CDATA[Artificial Intelligence]]></category>
		<category><![CDATA[Video]]></category>
		<category><![CDATA[AI agents]]></category>
		<category><![CDATA[AI security]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Darko Mesaros]]></category>
		<category><![CDATA[LLMs]]></category>
		<guid isPermaLink="false">https://shiftmag.dev/?p=13232</guid>

					<description><![CDATA[<p>Because AI agents are probabilistic and can't be trusted on their own, developers need to build layered safeguards around them, and the cost of running them in production deserves as much attention as safety.</p>
<p>The post <a href="https://shiftmag.dev/you-shouldnt-trust-ai-agents-in-production-13232/">You shouldn&#8217;t trust AI agents in production</a> appeared first on <a href="https://shiftmag.dev">ShiftMag</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Darko Mesaros, a Distinguished Developer Advocate at AWS, <strong>doesn&#8217;t trust his AI agents</strong>. We spoke with him after his talk at WeAreDevelopers San Jose about where to set limits for them and why production costs deserve more attention.</p>



<p class="wp-block-paragraph">He thinks that distrust is a sensible place to start. Agents can be wrong, so if you let them touch business operations, you need safeguards around their decisions.</p>



<p class="wp-block-paragraph">Let&#8217;s start with why he doesn&#8217;t trust them.</p>



<h2 class="wp-block-heading" id="your-talk-at-wad-san-jose-starts-with-a-strong-statement-i-don%e2%80%99t-trust-ai-agents-what-exactly-shouldn%e2%80%99t-developers-trust-about-them">Your talk at WAD San Jose starts with a strong statement: &#8220;I don’t trust AI agents.&#8221; What exactly shouldn’t developers trust about them?</h2>



<p class="wp-block-paragraph"><strong>Darko</strong>: The reason I don’t trust my AI agents is that they’re ultimately probabilistic systems. They’re not deterministic. When you give agents serious responsibilities, such as handling refunds, selling things or dealing with sensitive customer data, relying on a probabilistic system is a problem. <strong>Agents by themselves, without guardrails around them, are problematic</strong>.</p>



<p class="wp-block-paragraph">I’ve spent a lot of time teaching developers how to approach this. Building agents is more than having an agent and access to an LLM. It’s about building the system around it, with guardrails and everything you need to make the agent a little more secure.</p>



<h2 class="wp-block-heading" id="what%e2%80%99s-the-biggest-difference-between-an-ai-agent-that-works-in-a-demo-and-one-that-is-actually-ready-for-production">What’s the biggest difference between an AI agent that works in a demo and one that is actually ready for production?</h2>



<p class="wp-block-paragraph"><strong>Darko:</strong> Believe it or not, cost. When we build an agent for a demo, we might pick the biggest model because we want to make sure the demo works.</p>



<p class="wp-block-paragraph">The challenge comes when you start scaling that out. When an agent handles hundreds of thousands of customer requests in different ways, cost becomes an issue. <strong>Optimization isn’t something you worry about in a demo</strong>. You don’t worry about prompt caching, RAG or those things.</p>



<p class="wp-block-paragraph">In an actual production system, you start to consider cost. Beyond safety and guardrails, that’s the biggest difference between a demo agent and a real-life production agent.</p>



<h2 class="wp-block-heading" id="when-building-an-agent-what-safety-layer-should-developers-add-first-guardrails-permissions-observability-or-something-else">When building an agent, what safety layer should developers add first: guardrails, permissions, observability or something else?</h2>



<p class="wp-block-paragraph"><strong>Darko:</strong> The answer is yes to all of those things, because proper defense is built in layers. A single layer, a single guardrail, may not prevent everything. You may want guardrails, MCP input validation and deterministic policy checks around certain tool calls. All of those things are important.</p>



<p class="wp-block-paragraph">As I mentioned in my talk, observability becomes incredibly important <strong>because you really don’t know what your agents are doing</strong>, especially at that scale. Being able to observe them and run evaluations periodically or in real time gives you a lot more peace of mind. So, to answer the question of which one to add, the answer is yes.</p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe title="AWS Expert on 3 Must-Haves Before Shipping AI Agents" width="500" height="281" src="https://www.youtube.com/embed/Nv0uRM1W4V0?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div></figure>



<h2 class="wp-block-heading" id="how-do-you-know-when-an-ai-agent-has-made-a-mistake-if-its-output-still-looks-perfectly-reasonable">How do you know when an AI agent has made a mistake if its output still looks perfectly reasonable?</h2>



<p class="wp-block-paragraph"><strong>Darko:</strong> You have to combine a few things, and we go back to observability. Implementing automated tracing is critical so you can trace what’s going on in real time. But tracing doesn’t stop at the agent. You also need to trace the backend system the agent works with.</p>



<p class="wp-block-paragraph">If you start detecting anomalies, such as an increasing number of small or large orders, or something unusual, <strong>you need to look at both the agent and the backend</strong>. Combining those signals into a proper anomaly detection system is how you could potentially catch certain things happening in real time.</p>



<p class="wp-block-paragraph">It’s never just an agent. It’s an agent plus everything else behind it.</p>



<h2 class="wp-block-heading" id="how-much-autonomy-should-we-actually-give-ai-agents-in-production-where-should-developers-draw-the-line">How much autonomy should we actually give AI agents in production? Where should developers draw the line?</h2>



<p class="wp-block-paragraph"><strong>Darko:</strong> Ultimately, responsibility for what happens, for a system or code that goes into production, rests with the developer.</p>



<p class="wp-block-paragraph">How much autonomy should we give agents? <strong>It depends on the blast radius of a change or of a system going down</strong>. Is this a simple chatbot that tells customers the opening times of your business? Sure. If something goes wrong, it’s not horrible.</p>



<p class="wp-block-paragraph">But if the system handles banking information or sensitive data, or processes purchases, you may want to reduce its autonomy. It’s not only about the code or change being submitted; it’s also about the system you’re submitting it to. Unfortunately, the answer here is: it depends.</p>



<h2 class="wp-block-heading" id="if-a-developer-is-shipping-their-first-production-ai-agent-tomorrow-what-are-the-three-things-they-absolutely-need-to-have-in-place">If a developer is shipping their first production AI agent tomorrow, what are the three things they absolutely need to have in place?</h2>



<p class="wp-block-paragraph"><strong>Darko:</strong> Number one, proper observability. They need to be able to observe and monitor what their agent is doing, run evaluations against the models and their decisions and choices, <strong>and constantly monitor those things for changes</strong>.</p>



<p class="wp-block-paragraph">Number two, policies. A policy language such as Cedar lets you put policies in front of tool execution so you can prevent certain tools from executing.</p>



<p class="wp-block-paragraph">Number three, consider an established harness. It’s tempting to build your own because we’re all developers, but I encourage developers to consider something like the Strands SDK, LangGraph or LangChain. <strong>It makes things easier and lets you focus on your business problem</strong> and spend less time figuring out how to wrangle the agent.</p>


<figure class="wp-block-post-featured-image"><img fetchpriority="high" decoding="async" width="1200" height="620" src="https://shiftmag.dev/wp-content/uploads/2026/10/darko_main.jpg?x32039" class="attachment-post-thumbnail size-post-thumbnail wp-post-image" alt="" style="object-fit:cover;" srcset="https://shiftmag.dev/wp-content/uploads/2026/10/darko_main.jpg 1200w, https://shiftmag.dev/wp-content/uploads/2026/10/darko_main-766x396.jpg 766w, https://shiftmag.dev/wp-content/uploads/2026/10/darko_main-300x155.jpg 300w, https://shiftmag.dev/wp-content/uploads/2026/10/darko_main-1024x529.jpg 1024w" sizes="(max-width: 1200px) 100vw, 1200px" /></figure><p>The post <a href="https://shiftmag.dev/you-shouldnt-trust-ai-agents-in-production-13232/">You shouldn&#8217;t trust AI agents in production</a> appeared first on <a href="https://shiftmag.dev">ShiftMag</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>How to Review a 1,000+ File PR Without Losing Your Mind</title>
		<link>https://shiftmag.dev/how-to-review-a-1000-file-pr-without-losing-your-mind-13054/</link>
		
		<dc:creator><![CDATA[Mario Brcina]]></dc:creator>
		<pubDate>Thu, 29 Oct 2026 13:33:06 +0000</pubDate>
				<category><![CDATA[Artificial Intelligence]]></category>
		<category><![CDATA[Software Engineering]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[AI agents]]></category>
		<category><![CDATA[code review]]></category>
		<category><![CDATA[PR review]]></category>
		<guid isPermaLink="false">https://shiftmag.dev/?p=13054</guid>

					<description><![CDATA[<p>The scariest part of a 1,186-file PR is how quickly 15% coverage can start to feel like a thorough review.</p>
<p>The post <a href="https://shiftmag.dev/how-to-review-a-1000-file-pr-without-losing-your-mind-13054/">How to Review a 1,000+ File PR Without Losing Your Mind</a> appeared first on <a href="https://shiftmag.dev">ShiftMag</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Let me present you the PR in question: ticket <strong>DAED-190, &#8220;Switch gw edit to new UI stack&#8221;</strong>. </p>



<p class="wp-block-paragraph">In one PR, a colleague replaced the legacy JSP gateway-edit page with a React single-page app. Because that meant changing the UI stack, the backend had to move with it: new REST controllers, a new application service, new tests, a Maven frontend build, and an early commit that deleted everything nobody referenced anymore.</p>



<p class="wp-block-paragraph">The diff, against master:</p>



<pre class="wp-block-code"><code>1186 files changed, 19809 insertions(+), 473971 deletions(-)
</code></pre>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="393" src="https://shiftmag.dev/wp-content/uploads/2026/10/image-6-1024x393.png?x32039" alt="" class="wp-image-13224" srcset="https://shiftmag.dev/wp-content/uploads/2026/10/image-6-1024x393.png 1024w, https://shiftmag.dev/wp-content/uploads/2026/10/image-6-300x115.png 300w, https://shiftmag.dev/wp-content/uploads/2026/10/image-6-767x294.png 767w" sizes="(max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Numbers are inflated because bundled third-party frontend was committed before this PR. The right panel excludes it.</figcaption></figure>



<p class="wp-block-paragraph">Bitbucket helpfully informed me that <strong>only the first 1000 files are shown</strong>. That number was the whole reason this turned into a project of its own.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="567" height="103" src="https://shiftmag.dev/wp-content/uploads/2026/10/image-5.png?x32039" alt="" class="wp-image-13222" srcset="https://shiftmag.dev/wp-content/uploads/2026/10/image-5.png 567w, https://shiftmag.dev/wp-content/uploads/2026/10/image-5-300x54.png 300w" sizes="auto, (max-width: 567px) 100vw, 567px" /></figure>
</div>


<p class="wp-block-paragraph">So I sat down and asked myself: <em>how do I actually do this well?</em></p>



<h2 class="wp-block-heading" id="review-fails-through-lack-of-coverage-not-lack-of-skill"><strong>Review fails through lack of coverage, not lack of skill</strong></h2>



<p class="wp-block-paragraph"><strong>Doing it manually was not realistic</strong>. I&#8217;d read for days, hold a shrinking amount of the change in my head, get tired, and still miss things, and the things I&#8217;d miss would be the boring, important ones (a field that quietly lost its validation or an action that no longer exists).</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">The failure mode of manual review at this size is not &#8220;I review it wrong&#8221;, it&#8217;s &#8220;I review 15% of it and quietly convince myself that was enough&#8221;.</p>
</blockquote>



<p class="wp-block-paragraph">The obvious alternative, throwing the diff at an LLM and asking &#8220;review this&#8221;, is worse. The diff alone doesn&#8217;t fit in the context window. And even if it did, the agent has no baseline: &#8220;is this line correct?&#8221; is a much weaker question than &#8220;does this behave like the thing it replaces?&#8221; When you&#8217;re replacing a UI, the interesting question is never <em>does the new code look right</em>, it&#8217;s <em>does the new code still do what the old code did</em>. You can&#8217;t answer that from the diff.</p>



<p class="wp-block-paragraph">I needed an <strong>approach that kept the context small enough for the agent to be reliable, while still covering the whole change</strong>.</p>



<h2 class="wp-block-heading" id="what-i-had-going-for-me-and-what-i-didnt">What I had going for me (and what I didn&#8217;t)</h2>



<p class="wp-block-paragraph">Before choosing an approach, it&#8217;s worth being honest about the shape of the thing you&#8217;re reviewing.</p>



<p class="wp-block-paragraph"><strong>In my favor</strong>: this is a relatively small CRUD service with only a handful of flows. A list page, a copy flow, an edit form, not a sprawling distributed system with a dozen integration points. That matters, because it means the surface a rewrite can break is finite and enumerable. Users had already been testing the new UI on IO (Integrating Environment), so the obvious, everyday path (open a gateway, change a field, save) had human eyes on it before the PR ever reached me.</p>



<p class="wp-block-paragraph"><strong>Which is exactly why I didn&#8217;t trust it</strong>. People test the happy path. They click through the flow that works, confirm the screen looks right, and move on. Nobody spends their afternoon feeding a 4000-character string into a 100-column field, or pulling the session out from under a save.</p>



<p class="wp-block-paragraph">So I deliberately weighted the review toward the unhappy paths: missing validations, error handling, what happens when a write fails halfway through. A green light from testers on IO told me the good path worked but told me almost nothing about the bad ones.</p>



<p class="wp-block-paragraph"><strong>And one thing working against me</strong>: I already knew, from weeks of troubleshooting this service, that it had holes: plenty of its fields were not properly validated or handled to begin with. So &#8220;the new code matches the old code&#8221; was never going to be enough on its own. If the baseline is leaky, faithfully reproducing the baseline reproduces the leaks.</p>



<p class="wp-block-paragraph">That&#8217;s part of why the review ended up covering DB constraints and failure modes rather than stopping at parity, and a big reason so many of the findings turned out to be inherited rather than new.</p>



<h2 class="wp-block-heading" id="divide-and-conquer">Divide and conquer</h2>



<p class="wp-block-paragraph">Two things made the difference.</p>



<p class="wp-block-paragraph"><strong>First, clone both sides side by side.</strong> I checked out master and the PR branch into two separate directories:</p>



<pre class="wp-block-code"><code>./master/infobip-gateway-edit      ← the old JSP page, the baseline
./pr_branch/infobip-gateway-edit   ← the React/Bepo SPA</code></pre>



<p class="wp-block-paragraph">That sounds trivial but it changes the whole exercise. Now &#8220;what did master do here?&#8221; is a file read, not a guess. Every claim the review makes can be traced back to a line in the old code. The agent is comparing two concrete states, and not reasoning a diff. </p>



<p class="wp-block-paragraph">(I also cloned the two SQL repositories that hold the table definitions, because part of the review ended up being &#8220;does the new code respect the actual database constraints?&#8221;. More on that later.)</p>



<p class="wp-block-paragraph"><strong>Second, break the review into small steps and do one per session.</strong> This is the part that made the agent reliable. A single &#8220;review this PR&#8221; prompt would have produced a confident, shallow wall of text. Sixteen focused prompts produced something I could actually trust, because each one had a narrow scope, a written-down baseline, and a bounded amount of context to hold.</p>



<p class="wp-block-paragraph">The prompt I gave the agent did not say &#8220;review the changes&#8221;. It asked a narrower, more provable question, and it did so in very concrete terms.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="512" src="https://shiftmag.dev/wp-content/uploads/2026/10/additions_by_language-1024x512.png?x32039" alt="" class="wp-image-13208" srcset="https://shiftmag.dev/wp-content/uploads/2026/10/additions_by_language-1024x512.png 1024w, https://shiftmag.dev/wp-content/uploads/2026/10/additions_by_language-300x150.png 300w, https://shiftmag.dev/wp-content/uploads/2026/10/additions_by_language-768x384.png 768w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h3 class="wp-block-heading" id="the-instruction-that-started-it">The instruction that started it</h3>



<p class="wp-block-paragraph">Here is the first prompt in full: it names what we&#8217;re checking, the order, the output files, and how we&#8217;ll work together.</p>



<p class="wp-block-paragraph"></p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<details class="wp-block-details has-background is-layout-flow wp-block-details-is-layout-flow" style="background-color:#f5f5f5"><summary> <strong>&#8211; First-phase review prompt (<em>click to expand)</em></strong></summary>
<div class="wp-block-group has-background" style="background-color:#f5f5f5"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<h3 class="wp-block-heading" id="code-review-strategy">Code review strategy</h3>



<p class="wp-block-paragraph">Since PR is so large, DO NOT try to review it by listing changes and checking implementation correctness.</p>



<p class="wp-block-paragraph">Instead we will focus on:</p>



<ul class="wp-block-list">
<li>does branch version has all the fields and functionalities that were present on master</li>



<li>does branch version has all the validations that were present on master</li>
</ul>



<p class="wp-block-paragraph">This will be the only things we will focus in first part of the review.</p>



<p class="wp-block-paragraph">So we need to make a plan and do it in steps so we manage context efficiently.</p>



<p class="wp-block-paragraph">Here is my proposed order:</p>



<ol start="1" class="wp-block-list">
<li>Document all fields present on master (by checking source code of master branch) and store documentation in new md file (call it <code>./MASTER_FIELDS_DOCS.md</code>)</li>



<li>Document all functionalities/actions on master (that can be done by user) and store documentation in new md file (call it <code>./MASTER_ACTIONS_DOCS.md</code>)</li>



<li>Document all validations on master and store documentation in new md file (call it <code>./MASTER_VALIDATIONS_DOCS.md</code>)</li>



<li>At this step you should have all the docs needed (that will serve as reference), and you can start reviewing are all fields that were present on master (documented in step 1) present in PR branch</li>



<li>Same thing like previous point, in this step you will check are features/actions present on master (documented in step 2) also present in PR branch</li>



<li>And finally, you will check are validations present on master (documented in step 3) also present in PR branch</li>
</ol>



<p class="wp-block-paragraph">Before any of the above (as step 0) create review plan with ordered items (based on the above instructions) that will serve you and me to track the progress. Idea is that we have plan written in md file and then go over it step by step. After finishing one step you stop, update plan file and report back. I check result, compact session and give green light to continue to next session.</p>
</div></div>
</details>
</div></div>



<p class="wp-block-paragraph">Two things are doing most of the work here. The prompt leads with a <strong>negative</strong> instruction (<em>don&#8217;t</em> review the diff), which steers the agent away from the obvious-but-wrong approach. And it hands over a concrete first output (three named files), a stopping rule, and a coordination protocol (<em>stop, update the plan, report back</em>). None of that is clever; it&#8217;s just specific. That specificity is what kept sixteen sessions coherent.</p>



<h2 class="wp-block-heading" id="why-the-plan-lives-in-a-file">Why the plan lives in a file</h2>



<p class="wp-block-paragraph">The move that mattered most wasn&#8217;t clever. It was one of the earliest practices in agentic coding: a plan file (<code>REVIEW_PLAN.md</code>) with a status table at the top:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td>#</td><td>Step</td><td>Output file</td><td>Status</td></tr><tr><td>0</td><td>Create review plan</td><td>REVIEW_PLAN.md</td><td>DONE</td></tr><tr><td>1</td><td>Document all master fields</td><td>MASTER_FIELDS_DOCS.md</td><td>DONE</td></tr><tr><td>2</td><td>Document all master actions</td><td>MASTER_ACTIONS_DOCS.md</td><td>DONE</td></tr><tr><td>3</td><td>Document all master validations</td><td>MASTER_VALIDATIONS_DOCS.md</td><td>DONE</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">The workflow was:</p>



<ol start="1" class="wp-block-list">
<li>Agent does <strong>one</strong> step.</li>



<li>Agent <strong>stops</strong>, updates the status table, and reports back.</li>



<li>I read the result and decide if it&#8217;s good enough.</li>



<li>I compact the session (context reset), give the green light, and the agent starts the next step.</li>
</ol>



<p class="wp-block-paragraph">Two things come out of this. The agent never has to hold more than one step in mind, which keeps the context (and therefore the hallucination risk) small. And <strong>I</strong> never have to hold the whole PR in mind either, because the plan file does that for me. If I come back a week later, <code>REVIEW_PLAN.md</code> tells me exactly where we are. (This turned out to matter: I later re-checked comments after the developer pushed new commits, and the plan file meant I didn&#8217;t have to reconstruct anything.)</p>



<p class="wp-block-paragraph">The other side of the workflow: the intermediate documents are <em>baselines</em>. <code>MASTER_FIELDS_DOCS.md</code> lists every field on the old page with its type, options, and visibility rules. <code>MASTER_ACTIONS_DOCS.md</code> lists 17 groups of user actions.</p>



<p class="wp-block-paragraph"><code>MASTER_VALIDATIONS_DOCS.md</code> lists <strong>47 numbered validations</strong> (V1–V47). Each entry names its source file and line, so the parity check in a later step is mechanical rather than interpretive: you can diff a document against the new code, which is <em>much</em> more reliable than asking an agent to &#8220;look for missing validations&#8221;.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="374" src="https://shiftmag.dev/wp-content/uploads/2026/10/the_loop-1024x374.png?x32039" alt="" class="wp-image-13209" srcset="https://shiftmag.dev/wp-content/uploads/2026/10/the_loop-1024x374.png 1024w, https://shiftmag.dev/wp-content/uploads/2026/10/the_loop-300x110.png 300w, https://shiftmag.dev/wp-content/uploads/2026/10/the_loop-766x280.png 766w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">An overview of the loop.</figcaption></figure>



<h2 class="wp-block-heading" id="building-on-the-baseline-phase-2">Building on the baseline (phase 2)</h2>



<p class="wp-block-paragraph">Once the three parity axes were done (fields, actions, validations), I had a solid, evidence-backed answer to &#8220;did the rewrite lose anything?&#8221; That&#8217;s the question a UI rewrite can actually get wrong in a way users notice.</p>



<p class="wp-block-paragraph">But parity isn&#8217;t correctness. A rewrite can keep every field and every rule and still be broken in ways the old page wasn&#8217;t. So I extended the plan with a second phase, still small steps, still one per session, this time walking the application like an engineer building it from scratch and asking <em>which parts have to work for this to be safe to ship?</em></p>



<p class="wp-block-paragraph">I kicked off that phase with a second prompt, the same shape as the first: a concrete goal, the scope I was deliberately excluding, and an ordered list of steps.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<details class="wp-block-details has-background is-layout-flow wp-block-details-is-layout-flow" style="background-color:#f5f5f5"><summary><strong>&#8211; Second-phase review prompt (<em>click to expand</em>)</strong></summary>
<div class="wp-block-group has-background" style="background-color:#f5f5f5"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<h3 class="wp-block-heading" id="goal"><strong>Goal</strong></h3>



<p class="wp-block-paragraph">This is the second part of the review. In the first part we did NOT check the implementation details, we only checked that the branch version carries over everything that was on master (all fields, all functionalities/actions, all validations). That baseline is confirmed and documented now, and the comments that came out of it are already posted on the PR.</p>



<p class="wp-block-paragraph">Now we go one level deeper and review the PR implementation for correctness. Same rule as before: the PR is huge, so we will NOT do it line by line. We treat the app as a set of standalone parts and check each part in isolation, one step per session, so we manage the agent&#8217;s context efficiently.</p>



<p class="wp-block-paragraph">Code quality and code organisation are NOT the focus. Behaviour is. We will focus on correctness and checxk does the app work correctly and safely for the user.</p>



<h3 class="wp-block-heading" id="code-review-strategy-2"><strong>Code review strategy</strong></h3>



<p class="wp-block-paragraph">Instead of diffing against master, we walk the app the way an engineer building it from scratch would, and ask which parts have to work for it to be safe to ship. So we will check these, each as a standalone unit:</p>



<ul class="wp-block-list">
<li>are the DB constraints that back the form actually enforced (length, nullability, numeric width, uniqueness, CHECK), or do they only blow up later in SQL Server</li>



<li>are the writes atomic, and what happens when two operators change the same gateway at the same time</li>



<li>what does the user actually see when something fails (validation error, missing record, unexpected error)</li>



<li>does the new stack respect the same permissions the old tool required</li>



<li>is the new REST contract consistent with what the frontend sends and expects</li>



<li>does the SPA work at runtime (routing/deep links, races, i18n, keyboard/a11y)</li>



<li>what is cached (HTTP, Spring, browser) and can any of it serve stale content</li>



<li>keystore file upload: limits, validation, error path</li>



<li>what we can observe in production (logging, audit trail, metrics/health)</li>



<li>does the build produce the right assets, reproducibly</li>
</ul>



<h3 class="wp-block-heading" id="scope-notes"><strong>Scope notes</strong></h3>



<ul class="wp-block-list">
<li>Security: verify only that the authorization the old tool required is carried over. Do NOT review the SSO internals.</li>



<li>API contract: treat the old routes as internal (no external callers confirmed), so focus on internal consistency only.</li>



<li>The write flow that matters is COPY. Users only copy a template gateway; add/update and the auxiliary actions are not used today and are being removed.</li>
</ul>



<h3 class="wp-block-heading" id="here-is-my-proposed-order"><strong>Here is my proposed order</strong></h3>



<p class="wp-block-paragraph">Continue the numbering from the plan file you already have (steps 0-6 are done). For each step, record traceable findings plus a verdict, and write the output md file.</p>



<ol start="7" class="wp-block-list">
<li>Verify branch validations against DB constraints (length, nullability, numeric width, uniqueness, CHECK)</li>



<li>Data integrity, transactions &amp; concurrency</li>



<li>Error handling &amp; failure modes</li>



<li>Security: authorization transfer only</li>



<li>API contract &amp; integration (no external callers)</li>



<li>Frontend runtime correctness</li>



<li>Caching (HTTP + Spring + frontend)</li>



<li>File upload (keystore)</li>



<li>Observability &amp; operability</li>



<li>Build, packaging &amp; assets</li>
</ol>



<p class="wp-block-paragraph">Add these steps to the plan file and keep the same way of working: after finishing one step you stop, update the plan file and report back. I check the result, compact the session and give the green light to continue to the next step.</p>



<h3 class="wp-block-heading" id="files-you-should-read"><strong>Files you should read</strong></h3>



<p class="wp-block-paragraph">Read following files:</p>



<ol start="1" class="wp-block-list">
<li><code>./REVIEW_PLAN.md</code></li>



<li><code>./INSTRUCTIONS_FOR_AGENT.md</code></li>



<li><code>./MASTER_FIELDS_DOCS.md</code></li>



<li><code>./MASTER_ACTIONS_DOCS.md</code></li>



<li><code>./MASTER_VALIDATIONS_DOCS.md</code></li>



<li><code>./PR_COMMENT.md</code></li>



<li><code>./JIRA_TASK.md</code></li>
</ol>
</div></div>
</details>
</div></div>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td>#</td><td></td><td></td></tr><tr><td>7</td><td>Validations against <strong>DB constraints</strong> (length, nullability, numeric width, uniqueness, CHECK)</td><td>REVIEW_VALIDATIONS_AGAINST_DB.md</td></tr><tr><td>8</td><td><strong>Data integrity, transactions &amp; concurrency</strong></td><td>REVIEW_FINDINGS_DATA_INTEGRITY.md</td></tr><tr><td>9</td><td><strong>Error handling &amp; failure modes</strong></td><td>REVIEW_FINDINGS_ERROR_HANDLING.md</td></tr><tr><td>10</td><td><strong>Security</strong>: authorization transfer only</td><td>REVIEW_FINDINGS_AUTHORIZATION.md</td></tr><tr><td>11</td><td><strong>API contract &amp; integration</strong></td><td>REVIEW_FINDINGS_API_CONTRACT.md</td></tr><tr><td>12</td><td><strong>Frontend runtime correctness</strong></td><td>REVIEW_FINDINGS_FRONTEND_RUNTIME.md</td></tr><tr><td>13</td><td><strong>Caching</strong> (HTTP + Spring + frontend)</td><td>REVIEW_FINDINGS_CACHING.md</td></tr><tr><td>14</td><td><strong>File upload (keystore)</strong></td><td>REVIEW_FINDINGS_KEYSTORE_UPLOAD.md</td></tr><tr><td>15</td><td><strong>Observability &amp; operability</strong></td><td>REVIEW_FINDINGS_OBSERVABILITY.md</td></tr><tr><td>16</td><td><strong>Build, packaging &amp; assets</strong></td><td>REVIEW_FINDINGS_BUILD_ASSETS.md</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">I wrote these steps the way I&#8217;d audit a service: transactions, then failure paths, then who&#8217;s allowed in, then the contract, then the runtime, then the edges. Each one is a standalone unit of &#8220;does this part work?&#8221;, so the agent could check it in isolation without needing the other fifteen steps in context.</p>



<p class="wp-block-paragraph">Two of the steps earned their place in ways I didn&#8217;t predict:</p>



<ul class="wp-block-list">
<li><strong>DB constraints (step 7)</strong> turned into the single most valuable step. Comparing the form metadata against the actual DDL found that 43 text columns had no length enforcement anywhere, client <em>or</em> server, so an over-long value would travel all the way to SQL Server and come back as <em>&#8220;String or binary data would be truncated&#8221;</em>, with the save handler flattening every database error into one generic message. There&#8217;s no way to find that by reading a diff; you find it by resolving each form field to its column and checking the constraint is enforced somewhere.</li>



<li><strong>Caching (step 13)</strong> found that the new SPA loads its bundles from fixed, unversioned URLs, so a deploy could leave browsers running last week&#8217;s JavaScript against this week&#8217;s backend. That one was real, and it got fixed. (Which is its own small lesson: a review that produces at least one concrete, fixed bug has paid for itself.)</li>
</ul>



<h2 class="wp-block-heading" id="what-i-didnt-review-on-purpose">What I didn&#8217;t review <em>on purpose</em></h2>



<p class="wp-block-paragraph">I ignored code quality and organization. Not because it doesn&#8217;t matter, but because I had to choose, and at 1186 files I couldn&#8217;t do everything (see &#8220;the line&#8221; below). What I prioritised was <strong>app correctness and the things a user would notice</strong>. These include a missing field, a missing validation, a save that half-commits, an error page instead of an error message or a bundle that won&#8217;t refresh.</p>



<p class="wp-block-paragraph">I&#8217;m confident there is dead code left in that PR. There is certainly some code that could be written more clearly, or factored differently. I flagged the small amount of dead code I tripped over while looking for correctness issues (unused imports, unreferenced configuration properties), but I didn&#8217;t go looking for it.</p>



<p class="wp-block-paragraph">That was a conscious trade, not an oversight.</p>



<h2 class="wp-block-heading" id="tools">Tools</h2>



<p class="wp-block-paragraph">I used <strong>Pi</strong> with an AI model served from our own infrastructure.</p>



<p class="wp-block-paragraph">I keep Pi deliberately bare (no MCPs and no skills) because I also use it with models running locally on my Mac, where RAM limits how much context I can hold, so I keep the harness lean to leave as much of the window as possible for the actual work. This review ran against the company-hosted model, so that constraint didn&#8217;t really bite here, but the bare setup is what I&#8217;m used to.</p>



<p class="wp-block-paragraph">When a task genuinely needs tooling, I reach for other agents (Codex, Claude Code) that have it wired up. For this review what I needed wasn&#8217;t tools; it was a patient reader with one small, well-defined job per session, and that&#8217;s what the step-by-step structure provided.</p>



<h2 class="wp-block-heading" id="what-the-review-produced">What the review produced</h2>



<p class="wp-block-paragraph">All told, the process produced 16 review steps, 13 findings documents, and a set of PR-ready comments. Not every step produced a comment: several produced an explicit <em>&#8220;no regression, no comment needed&#8221;</em> verdict, which is just as valuable, because it&#8217;s a documented answer instead of a silent omission. A few of the things it turned up:</p>



<ul class="wp-block-list">
<li><strong>A real caching bug</strong> in the new SPA bundle URLs (found, commented, <strong>fixed by the developer</strong>).</li>



<li><strong>A dropped keystore</strong> on copy-without-reupload (the copy path read the source gateway and then discarded it, so the row ended up with the old filename and password but a <code>NULL</code> keystore blob).</li>



<li><strong>A blank page</strong> when the SPA is opened at a URL that isn&#8217;t a copy URL: the URL parser throws <em>before</em> the promise <code>.catch</code> that renders the &#8220;cannot open the gateway&#8221; message, and there&#8217;s no error boundary, so the operator gets nothing.</li>



<li><strong>The dead</strong> <code>action</code> <strong>envelope field</strong>, and assorted smaller items.</li>



<li>And on the other side, a genuine <strong>improvement</strong> worth calling out: the branch is <em>stricter</em> than master on server-side bounds: 18 <code>@Range</code> checks and the <code>GatewaySpeed</code> <code>@DecimalMin/Max</code> rules now run on save where master had them dormant. I asked the developer to confirm the stricter rejections were intentional, but it&#8217;s a good change.</li>
</ul>



<p class="wp-block-paragraph">A handful of the findings resolved in the developer&#8217;s favour, too: two &#8220;missing&#8221; grids turned out to be non-functional on master in the first place (their buttons were disabled and the permission helper always returned <code>false</code>), and one authorization concern from the plan turned out not to be a real gate at all. </p>



<p class="wp-block-paragraph">Part of doing this well is being willing to be wrong in writing, and the baseline documents made that checkable rather than a matter of opinion.</p>



<h2 class="wp-block-heading" id="the-line-you-have-to-draw">The line you have to draw</h2>



<p class="wp-block-paragraph">Somewhere in a PR this size you have to decide you&#8217;re done. You will not read everything. You will miss nuances. There is dead code you didn&#8217;t flag and duplication you didn&#8217;t mention.</p>



<p class="wp-block-paragraph">That&#8217;s not failure. It&#8217;s the job. The alternative is holding a 1186-file change to an impossible standard and never shipping the review. So I drew the line at app correctness, accepted that code-quality nits would survive, and moved on, because other things were waiting for my attention.</p>



<p class="wp-block-paragraph">If you take one thing from this: <strong>scope the review to a small number of axis you can actually prove</strong>, write the baseline down, and let the structure (not willpower) keep you honest.</p>



<h2 class="wp-block-heading" id="credit-where-its-due">Credit where it&#8217;s due</h2>



<p class="wp-block-paragraph">A list of findings can give the wrong impression, so I want to be explicit about something: <strong>most of what this review flagged was not introduced by this PR.</strong> It was copied, faithfully, from the old master implementation.</p>



<p class="wp-block-paragraph">The non-atomic copy (step 8), the generic &#8220;database error&#8221; that names no field (step 7), the audit that is best-effort and swallows its own failures (step 15), the permission helper that always returned <code>false</code>, the grids whose buttons did nothing (steps 5/10), the lengthless text columns: the new code reproduces the old behaviour, warts and all. </p>



<p class="wp-block-paragraph">That is exactly what a rewrite is <em>supposed</em> to do, and finding inherited behaviour is not the same as finding a defect. I said as much in the write-ups: several findings are marked &#8220;parity&#8221; or &#8220;no regression&#8221; rather than &#8220;bug&#8221; for precisely that reason.</p>



<p class="wp-block-paragraph">Which reframes the numbers. In a PR this size (a full UI stack replacement, plus the backend changes to support it), the count of genuinely <em>new</em> defects was small. Most of the review&#8217;s output is a map of what the old system already did, not a list of things the rewrite broke.</p>



<p class="wp-block-paragraph">Large part of that is the developer&#8217;s work. He kept parity deliberately, wrote tests that pin behaviour, and fixed the real issues (the caching bug, the keystore drop, the case-sensitivity bug in the new field check) as they came up. But I also think it&#8217;s a genuine sign of how far LLM-assisted development has come. </p>



<p class="wp-block-paragraph">Producing a large, coherent, mostly-correct rewrite, where the majority of what a careful reviewer finds is the <em>old</em> code&#8217;s pre-existing behaviour rather than new mistakes, is not something I would have expected a couple of years ago. The human still matters: someone had to guide the change, land it, and decide what was worth reviewing. </p>



<p class="wp-block-paragraph">But the baseline quality of the large machine-written change was high, and the remaining edge cases were the exceptions, not the rule.</p>



<h2 class="wp-block-heading" id="caveats">Caveats</h2>



<p class="wp-block-paragraph">This is <strong>one developer&#8217;s approach to one specific PR</strong>. It is not a general methodology and it isn&#8217;t a claim that everyone should review big PRs this way.</p>



<p class="wp-block-paragraph">It worked well <em>here</em> for a particular reason: this PR was a <strong>rewrite of an existing behaviour</strong>, which gives you a natural baseline (the old code) and a natural set of questions (does the new code still do all of it?). A 1000-file PR that <em>adds</em> a new feature has no such baseline, and the same divide-and-conquer axes wouldn&#8217;t transfer directly. </p>



<p class="wp-block-paragraph">A PR that&#8217;s mostly dependency bumps or generated code would want something else again.</p>



<p class="wp-block-paragraph">Treat it as a worked example you can borrow from, not a rulebook. The transferable part isn&#8217;t the sixteen steps. It&#8217;s the habits: clone both sides, pick a small number of provable axes, write the plan down, keep each session narrow, and stop when you&#8217;ve covered what you decided mattered.</p>


<figure class="wp-block-post-featured-image"><img loading="lazy" decoding="async" width="1280" height="720" src="https://shiftmag.dev/wp-content/uploads/2026/10/ShiftMag-article-banner-4-1.png?x32039" class="attachment-post-thumbnail size-post-thumbnail wp-post-image" alt="" style="object-fit:cover;" srcset="https://shiftmag.dev/wp-content/uploads/2026/10/ShiftMag-article-banner-4-1.png 1280w, https://shiftmag.dev/wp-content/uploads/2026/10/ShiftMag-article-banner-4-1-300x169.png 300w, https://shiftmag.dev/wp-content/uploads/2026/10/ShiftMag-article-banner-4-1-1024x576.png 1024w, https://shiftmag.dev/wp-content/uploads/2026/10/ShiftMag-article-banner-4-1-768x432.png 768w" sizes="auto, (max-width: 1280px) 100vw, 1280px" /></figure><p>The post <a href="https://shiftmag.dev/how-to-review-a-1000-file-pr-without-losing-your-mind-13054/">How to Review a 1,000+ File PR Without Losing Your Mind</a> appeared first on <a href="https://shiftmag.dev">ShiftMag</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Two years later, I know what I got wrong about typed domain errors and what I’d keep</title>
		<link>https://shiftmag.dev/two-years-later-i-know-what-i-got-wrong-about-typed-domain-errors-and-what-id-keep-12915/</link>
		
		<dc:creator><![CDATA[Karlo Bratko]]></dc:creator>
		<pubDate>Tue, 27 Oct 2026 13:53:59 +0000</pubDate>
				<category><![CDATA[Programming Languages]]></category>
		<category><![CDATA[Software Engineering]]></category>
		<category><![CDATA[Kotlin]]></category>
		<guid isPermaLink="false">https://shiftmag.dev/?p=12915</guid>

					<description><![CDATA[<p>This is a personal engineering essay, not a blueprint. I don’t know what the perfect service architecture looks like, and I won’t pretend I do. I’m not trying to sell this approach, I just want to share what I learned, in case it helps someone else avoid a few wrong turns.</p>
<p>The post <a href="https://shiftmag.dev/two-years-later-i-know-what-i-got-wrong-about-typed-domain-errors-and-what-id-keep-12915/">Two years later, I know what I got wrong about typed domain errors and what I’d keep</a> appeared first on <a href="https://shiftmag.dev">ShiftMag</a>.</p>
]]></description>
										<content:encoded><![CDATA[<figure class="wp-block-post-featured-image"><img loading="lazy" decoding="async" width="1200" height="630" src="https://shiftmag.dev/wp-content/uploads/2026/10/ShiftMag-article-banner-2-1.png?x32039" class="attachment-post-thumbnail size-post-thumbnail wp-post-image" alt="" style="object-fit:cover;" srcset="https://shiftmag.dev/wp-content/uploads/2026/10/ShiftMag-article-banner-2-1.png 1200w, https://shiftmag.dev/wp-content/uploads/2026/10/ShiftMag-article-banner-2-1-300x158.png 300w, https://shiftmag.dev/wp-content/uploads/2026/10/ShiftMag-article-banner-2-1-1024x538.png 1024w, https://shiftmag.dev/wp-content/uploads/2026/10/ShiftMag-article-banner-2-1-768x403.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px" /></figure>


<p class="wp-block-paragraph">I love this style: <strong>a function’s expected failures are spelled out in its signature as a sealed type</strong>, rather than hidden in its body.</p>



<p class="wp-block-paragraph">A couple of years ago, I tried something very similar in a toy project, but I never saw the contract benefit. The style wasn’t the problem, and doing more of it wasn’t the fix. </p>



<p class="wp-block-paragraph">This essay responds to a JetBrains blog post about <strong>typed domain errors</strong>. I agree with its diagnosis, though I’m clearer about what not to do than about the ideal approach. I also share what I got wrong, in the hope you can avoid the same mistakes.</p>



<h2 class="wp-block-heading" id="the-blog-post-im-responding-to">The blog post I&#8217;m responding to</h2>



<p class="wp-block-paragraph">Sergey Chernov&#8217;s <a href="https://blog.jetbrains.com/kotlin/2026/08/signatures-be-true-domain-errors-and-functional-handling-in-kotlin/" target="_blank" rel="noreferrer noopener">&#8220;Signatures, be true: domain errors and functional handling in Kotlin&#8221;</a> makes a claim I strongly agree with: a signature like <code>fun signDocument(id, code): Unit</code> that throws in secret is <strong>lying about what it can do</strong>, and the fix is to put the expected failures in the type.</p>



<p class="wp-block-paragraph">His core contribution is the <strong>three-category taxonomy</strong>, and it&#8217;s the most useful part of the post.</p>



<ol class="wp-block-list">
<li><strong>API client misuse, e.g. malformed JSON, a missing header or an out-of-sequence request</strong>. Collapse these to a coarse 4xx and keep them out of the domain model. Access control is a partial exception. A bare authentication failure is transport-level and stays out, but an ownership or permission decision that depends on business state often belongs in the union, because the caller needs the specific reason and not just a 403.</li>



<li><strong>Unexpected exceptions, e.g. the database being down, a timeout, a dropped network or a bug</strong>. Throw these, and let the global handler turn them into a 500 with a stack trace and an on-call page.</li>



<li><strong>Domain errors, e.g. a wrong sign code, a closed window or an already-signed document</strong>. This is the only category that belongs in the contract, as a sealed type.</li>
</ol>



<p class="wp-block-paragraph">A <strong>sealed type</strong>, in case that term is unfamiliar, is a closed hierarchy where the compiler knows every subtype. That is what makes a <code>when</code> over it exhaustive, with no <code>else</code> branch, and it&#8217;s the whole game.</p>



<p class="wp-block-paragraph">Everything else in the post is implementation. It covers a sealed interface for the error union, an <code>Either&lt;DomainError, T&gt;</code> return, exhaustive <code>when</code>, HTTP mapping only at the route boundary (the Ktor layer that owns the routes). It also publishes the error types alongside the API client, and its central recommendation is one narrow union per public method, rather than one shared union across a class. Here <code>Either</code> is just a value that is either a <code>Left</code> failure or a <code>Right</code> success. The line I would keep is <strong>&#8220;the contract is the unit of review&#8221;</strong>, which means you read the types rather than the 200 lines underneath. That is doubly true when an AI wrote the implementation.</p>



<p class="wp-block-paragraph">The post also <strong>skips over how to compose heterogeneous error unions</strong>, which is real work I had to figure out on my own. Real flows fail per step with different unions. A plain <code>either { }</code> block, which chains fallible steps and short-circuits on the first failure, infers a single left type from all of them. Mixing unrelated domain errors therefore gives you their least upper bound, meaning the most specific common supertype the compiler can find. When the types are unrelated, that is <code>Any</code>, which is the open-error problem the post warns about. A shared sealed interface is different, since it stays closed, but a union that broad is the contract bloat the post warns about elsewhere.</p>



<p class="wp-block-paragraph">The post is also <strong>quiet about how to observe legitimate domain values</strong>. It says even less about the cost of mis-categorizing infrastructure failures as domain errors, which I ran into myself. The same open-type reasoning is why <code>Result&lt;T&gt;</code> is a poor fit for domain modeling. Its failure side is <code>Throwable</code>, so a <code>when</code> over the failure value always needs an <code>else</code> and the contract gains nothing.</p>



<h2 class="wp-block-heading" id="a-note-on-the-boundary-api-client-misuse-vs-validation">A note on the boundary: API-client misuse vs validation</h2>



<p class="wp-block-paragraph">One line the taxonomy leaves murky is <strong>the line between API client misuse and validation</strong>. I read that line as two different things the post merges.</p>



<p class="wp-block-paragraph">The first is <strong>structural or transport validation</strong>, such as malformed JSON, a missing field or a wrong wire type. This is genuine client misuse. A healthy client never produces it and no designed screen is needed, so a coarse 400 is enough.</p>



<p class="wp-block-paragraph">The second is <strong>semantic validation</strong>, where a well-formed field violates a real rule and carries a meaningful message, like <code>TooShortUsername</code> or <code>NoDigitsInPassword</code>. Here the client benefits from knowing exactly which field is wrong and why, and often from getting all of the problems at once rather than just the first one.</p>



<p class="wp-block-paragraph"><strong>Where the exact line falls is not well-defined</strong>, as it depends on the use case and on the implementor&#8217;s preference. Some APIs deliberately collapse semantic validation to a coarse 400. Others return rich, accumulated field errors, which is what my validation tree did and it&#8217;s the part I still like. My own leaning is to let structural junk stay a coarse 400 at the framework boundary. Semantic validation can come back as a value, accumulated, but kept as its own validation-result type rather than folded into the business domain union.</p>



<p class="wp-block-paragraph">One wrinkle deserves naming. A rule like &#8220;this email is already taken&#8221; looks like validation, but it reads persisted state, so it&#8217;s really a domain conflict and belongs in the domain union, not in the request-validation result. It&#8217;s a fair criticism of the taxonomy that the first category needs this much boundary work, but it&#8217;s still a better starting point than nothing. I won&#8217;t pretend there is a single correct division here. It&#8217;s a judgment call for each API.</p>



<h2 class="wp-block-heading" id="my-case-study-a-good-design-at-the-wrong-layer">My case study: a good design at the wrong layer</h2>



<p class="wp-block-paragraph">In 2024 I built a <strong>Ktor/Kotlin/Arrow backend</strong> (<a href="https://github.com/karlobratko/instakt" target="_blank" rel="noopener">instakt</a>, now an unmaintained toy project) and went fully functional on error handling, but with no grouping by use case. This is how the style gets misused, and the taxonomy above is the frame I wish I had back then, because without it I mislabeled infrastructure failures as domain outcomes.</p>



<p class="wp-block-paragraph">The design was coherent in principle. One wide <a href="https://github.com/karlobratko/instakt/blob/master/app/src/main/kotlin/hr/kbratko/instakt/domain/error.kt" target="_blank" rel="noopener">root union</a> <code>DomainError</code> held a large tree of cases, and my database bucket alone had about 26 of them. Services mostly returned <code>EitherNel&lt;DomainError, T&gt;</code>, though a few methods returned a non-accumulating <code>Either&lt;DomainError, T&gt;</code> instead. <code>EitherNel</code> is an <code>Either</code> whose failure side is a <code>NonEmptyList</code>, which is how several errors get accumulated.</p>



<p class="wp-block-paragraph">A single <a href="https://github.com/karlobratko/instakt/blob/master/app/src/main/kotlin/hr/kbratko/instakt/infrastructure/errors/error-conversion.kt" target="_blank" rel="noopener">total mapper</a> <code>DomainError -&gt; HttpStatusCode</code> owned all of the exhaustiveness. It handled every case and returned a status, while the response body carried the error detail. It looked elegant and it compiled. Reading it back, three things were wrong.</p>



<p class="wp-block-paragraph">The most obvious problem was that <strong>the per-method contract was gone</strong>. Every method returned the whole <code>DomainError</code> union. A signature like <code>upload(...): EitherNel&lt;DomainError, ContentMetadata&gt;</code> told a reader almost nothing about which failures that one call could produce. The blog&#8217;s whole point, that the signature tells you what can go wrong for <em>this</em> call, was silently surrendered. Because the central mapper owned exhaustiveness, I avoided the impossible-branch work, but the signature stopped telling the truth.</p>



<p class="wp-block-paragraph">The problem I most regret was <strong>observability</strong>. I had modeled infrastructure failures, a failed persist or a failed mail send, as union values. My <code>exception&lt;Throwable&gt;</code> handler logged only <code>cause.message</code> with no stack trace. Between the two, infrastructure failures were turned into a status by the mapper and never reached the global handler at all. That meant no log entry, no trace and no error-rate spike, which is exactly why the blog says unexpected exceptions should throw. Grouping errors by use case would not have fixed this, and I want to be explicit about that. It was a separate mistake that needs a separate fix.</p>



<p class="wp-block-paragraph">The last problem was <strong>bloat</strong>. A shared root collects one more case from every operation, so there was no natural place to stop. Discovery got worse too, because every new failure landed next to unrelated ones.</p>



<h3 class="wp-block-heading" id="what-was-actually-good-validation">What was actually good: validation</h3>



<p class="wp-block-paragraph">The part that aged best is validation, and that&#8217;s not a coincidence. Validation is the one concern where <strong>the design matched the shape of the problem</strong>. A form wants all of its errors at once rather than the first one, so the accumulating checks served a genuine UX need. Nowhere else in the project was accumulation load-bearing. Field validation is also the purest domain concern, because it judges the request itself and never needs the database, storage or infrastructure.</p>



<p class="wp-block-paragraph"><strong>The nested tree earns itself here too</strong>, since field-level messages and group-level routing are two different consumers of the same structure. Each check is also a self-describing type such as <code>ContentSizeIsValid</code>, so a failure names exactly what was checked, even if the surrounding method signatures were still too wide to be useful.</p>



<p class="wp-block-paragraph">The lesson I draw is that <strong>error handling works best when it&#8217;s shaped by the operation&#8217;s structure</strong>. In my experience independent checks want to aggregate, while sequential business steps want to short-circuit or narrow per use case. Validation happened to be the only thing that was structurally an aggregate problem, so it came out right almost by accident. Aggregate independent checks (like validation); short-circuit sequential business steps with a narrow, use-case-specific outcome. Validation happened to be the only thing that was structurally an aggregation problem, so it came out right almost by accident.</p>



<h2 class="wp-block-heading" id="what-id-try-next">What I&#8217;d try next</h2>



<p class="wp-block-paragraph">I haven’t tested these in production, so treat them as hypotheses, not a proven design. The change I&#8217;d make first is to <strong>group by use case</strong>. By use case I mean the operation the caller asked for, not the function that happens to serve it. Uploading a profile photo and deleting a document are different use cases even when they share the same repository, and each one gets its own closed union of the failures it can produce. The wide union then lives only at the transport layer, where a single mapper to HTTP status genuinely belongs.</p>



<p class="wp-block-paragraph">That leaves <strong>the composition question</strong>. A narrow service union widens into the transport union through a small total mapping at the boundary, and that mapping is where the composition happens. One function there turns each <code>UploadError</code> into the matching case of the wide union, as in <code>UploadError -&gt; DomainError</code>, while the signature itself stays narrow, something like <code>fun upload(...): UploadError</code>. When a flow runs several steps, its union collects the cases those steps can produce that the caller must distinguish. That mapping is total, so adding a case stops the boundary compiling until it is handled, and it works the same whether the result is wrapped in an <code>Either</code> or a hand-written outcome type. It is deliberate and named, which separates it from the inferred collapse I described earlier, and it is acceptable only because the broad union never reaches a service signature. The cost is more up-front ceremony, a set of small types and a mapping per use case, but I think it&#8217;s worth it because the contract becomes readable, and I&#8217;d keep each mapping next to the type it converts so a new case stays a local edit.</p>



<p class="wp-block-paragraph">For the result type, I now prefer <strong>plain <code>when</code> plus hand-written per-use-case outcome types over <code>Either</code></strong>. I tried <code>Either</code>, and while it reads fine, the machinery around it wears on me. The cost is the Arrow <code>either { }</code> builder plus the chained calls that unwrap a success or short-circuit on a failure. It is not the <code>Either</code> type itself, which is a small sealed pair. As the years pass I&#8217;ve come to prefer the simplicity of a <code>when</code> and a set of small named types. This is a matter of taste, but it&#8217;s where I lean today. I&#8217;d also match the mechanism to the shape of the work. In my experience validation wants to accumulate, while a business flow wants to short-circuit through a narrow outcome type. I see no reason to force one mechanism everywhere.</p>



<p class="wp-block-paragraph"><strong>Observability needs its own attention</strong>, and it matters more in this style, not less. Going from exceptions to domain errors means you lose the stack trace unless you deliberately keep it. So I&#8217;d log expected domain failures at info or debug with a correlation ID. The global handler would keep the error-level logging for thrown exceptions, so nothing gets logged twice. I&#8217;d still throw for genuine unexpected exceptions, the ones that cannot be categorized as domain or client. The rule I hold myself to is to build domain errors from data I already have and never wrap raw database or network failures into the union. That rule is still coarse. A bounced email is a domain outcome, while an unreachable mail server is not. The real test is whether a correct client could still hit the failure and act on it.</p>



<p class="wp-block-paragraph"><strong>Narrow unions</strong> are also self-documenting in a way a wide one is not. A signature that returns a narrow, per-use-case error type tells a reader what can fail, while one that returns <code>DomainError</code> tells them almost nothing. For context, an early proposal in the Kotlin Evolution and Enhancement Process, <a href="https://github.com/Kotlin/KEEP/blob/main/proposals/KEEP-0441-rich-errors-motivation.md" target="_blank" rel="noopener">KEEP-0441 &#8220;Rich Errors, aka Error Union Types&#8221;</a>, would add error-union types to the language. It&#8217;s still at the design review stage, so it isn&#8217;t accepted or promised, and it isn&#8217;t an Arrow-specific idea. The error contract also has to survive releases. The concrete hazard is that adding a case to a shared sealed type is a breaking change for every client whose <code>when</code> is exhaustive. It also breaks serialization when an unknown case arrives from a newer producer. I&#8217;d treat an unknown incoming case as a defined fallback rather than a crash. This matters far less when the consumers are ours to rebuild in lockstep, as with internal models, and much more when a customer-facing connector is involved.</p>



<p class="wp-block-paragraph">On conventions I don&#8217;t think there is one right answer, since<strong> it always depends on the use case</strong>. I like names that are clear and descriptive. I&#8217;d use a <code>data class</code> whenever an error carries data, and <code>object</code> or <code>data object</code> when it does not. A <code>data object</code> needs Kotlin 1.9 or later, and a <code>data class</code> requires at least one property. I lean toward carrying data where I can, because an error that carries a payload is easier to debug and test than one that carries nothing. For nesting, validation seems to benefit from it, while in general I&#8217;d strive for <strong>flat, per-use-case</strong> errors, though there&#8217;s no universal rule and it has to be tested in context. Testing gets easier too, because you assert on the returned error value directly instead of catching a thrown type.</p>



<h2 class="wp-block-heading" id="where-i-land">Where I land</h2>



<p class="wp-block-paragraph"><strong>I still reach for this style, only with clearer boundaries</strong>. The blog&#8217;s taxonomy and its &#8220;contract is the unit of review&#8221; idea are worth keeping. Its flat &#8220;one narrow union per method&#8221; advice is a reasonable default at the level of the operation, though not for every helper function. A wide union with a central mapper still has a home at the transport layer.</p>



<p class="wp-block-paragraph">The honest summary of my two years is that <strong>a coherent wide-union pipeline is a defensible transport design, but it was the wrong layer for it</strong>. I can&#8217;t promise the approach I&#8217;d try next is the right architecture either, because I haven&#8217;t built it yet. What I&#8217;m confident about is the failure mode. My first attempt taught me that the hard way, and I hope it saves you the tuition.</p>
<p>The post <a href="https://shiftmag.dev/two-years-later-i-know-what-i-got-wrong-about-typed-domain-errors-and-what-id-keep-12915/">Two years later, I know what I got wrong about typed domain errors and what I’d keep</a> appeared first on <a href="https://shiftmag.dev">ShiftMag</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Todoist Founder: Developers Should Focus on Solving Customer Problems</title>
		<link>https://shiftmag.dev/todoist-founder-ai-startups-too-late-12943/</link>
		
		<dc:creator><![CDATA[Ivan Brezak Brkan]]></dc:creator>
		<pubDate>Mon, 26 Oct 2026 11:58:34 +0000</pubDate>
				<category><![CDATA[Artificial Intelligence]]></category>
		<category><![CDATA[Event]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[Amir Salihefendic]]></category>
		<category><![CDATA[Infobip Shift 2026]]></category>
		<category><![CDATA[todoist]]></category>
		<guid isPermaLink="false">https://shiftmag.dev/?p=12943</guid>

					<description><![CDATA[<p>I spoke with Todoist founder and CEO Amir Salihefendic about where developers and founders can find opportunities and what they should focus on next.</p>
<p>The post <a href="https://shiftmag.dev/todoist-founder-ai-startups-too-late-12943/">Todoist Founder: Developers Should Focus on Solving Customer Problems</a> appeared first on <a href="https://shiftmag.dev">ShiftMag</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><a href="https://shiftmag.dev/doist-ceo-the-bar-to-create-has-dropped-but-the-quality-bar-has-risen-10479/" target="_blank" rel="noreferrer noopener">Amir Salihefendic has been building Todoist for 20 years</a>, but he also started his career as a developer who gradually discovered his entrepreneurial ambitions. That experience shaped his talk at the<a href="https://shiftmag.dev/tag/infobip-shift-2026/" target="_blank" rel="noreferrer noopener"> Shift conference,</a> where he focused on developers who are considering starting their own companies.</p>



<p class="wp-block-paragraph">As someone who has spent nearly 20 years following developers, founders, and tech entrepreneurship, I spoke with Amir after his talk about what developers need to know to become founders. We discussed <strong>how AI is changing software development</strong>, junior hiring, product building, and what one person can achieve.</p>



<h2 class="wp-block-heading" id="developers-are-not-doomed">Developers are not doomed</h2>



<p class="wp-block-paragraph">What happens to developers now that AI can write code? It has probably become one of the most common questions at tech conferences over the past few years, ever since the AI revolution began. Naturally, I opened my conversation with Amir by asking exactly that.</p>



<p class="wp-block-paragraph">Amir has programmed since childhood and <strong>spent over 20 years developing his own Vim configuration</strong>, which he says became one of the world’s most popular. Today, he barely uses the editor the same way and rarely reviews code in detail (except when security, models, or other areas need his attention).</p>



<p class="wp-block-paragraph">Still, he doesn’t think developers are becoming irrelevant. Quite the opposite:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">My contrarian view on this is: developers are still super critical, because I don&#8217;t think code was ever the objective. What is objective? Solving problems for customers, building systems.</p>
</blockquote>



<p class="wp-block-paragraph">In other words, writing code was never the whole point. Code serves as a tool, <strong>while solving customer problems and building systems remain the real goals</strong>.</p>



<p class="wp-block-paragraph">What changes now is the level at which developers work. Managing agents, configuring them, coordinating multiple systems, and working with what Amir calls agent swarms all matter more and more.</p>



<p class="wp-block-paragraph">That shift also dramatically changes the leverage a single developer can have:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">I think the difference between a good developer and an amazing developer is not a 10x anymore. It&#8217;s 1000x.</p>
</blockquote>



<h2 class="wp-block-heading" id="juniors-may-have-an-advantage-that-seniors-do-not">Juniors may have an advantage that seniors do not</h2>



<p class="wp-block-paragraph">That raises another question: how can someone who enters the industry today learn what good engineering actually looks like?</p>



<p class="wp-block-paragraph">Amir sees a potential advantage for junior developers here. Unlike seniors, <strong>they do not carry 20 years of habits that they first need to unlearn</strong> &#8211; they can start from first principles and treat agents, agent swarms, or systems that build other systems as a natural part of development, instead of trying to retrofit them into a workflow they have followed in the same way for years.</p>



<p class="wp-block-paragraph">But that advantage has its limits: AI models still cannot replace the kind of experience that helps developers recognize whether a solution is actually good:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">This said, I do still think the models are not good enough for a lot. Especially architecture.</p>
</blockquote>



<h2 class="wp-block-heading" id="customers-are-still-p1-for-founders">Customers are still P1 for founders</h2>



<p class="wp-block-paragraph">AI may let one person build more, but a founder’s main job is still the same.</p>



<p class="wp-block-paragraph">I asked Amir what developers should focus on when they also have to think about design, marketing, sales, hiring, and fundraising. Once again, he came back to the basics: <strong>customers</strong>:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Again, I&#8217;m actually not sure how much has changed, because I think in the end what you&#8217;re building for is customers, it&#8217;s other people, it&#8217;s empowering them. And you need to find out: how can you get customers? How can you build something useful? And that is maybe the P1. Everything else, attending conferences, getting investment, should not be a P1.</p>
</blockquote>



<p class="wp-block-paragraph">He says <strong>new founders often reverse that order</strong>. They start with incubators, try to raise money, or look for a co-founder, and only then ask whether anyone actually needs what they are building.</p>



<p class="wp-block-paragraph">None of those things lack value. They simply do not belong at P1:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">But P1 is finding customers, finding something useful, building something useful, trying it out, learning from that, getting kicked in your face. And then you go back and you rebuild and you learn. And also this loop is very critical.</p>
</blockquote>



<p class="wp-block-paragraph">In other words, build something, put it in front of users, make mistakes, learn from them, and build again. Amir sees that loop as one of the key parts of building a company.</p>



<h2 class="wp-block-heading" id="do-not-blindly-copy-silicon-valley">Do not blindly copy Silicon Valley</h2>



<p class="wp-block-paragraph">From the very beginning, Todoist followed a different organizational model from the classic Silicon Valley playbook, but Amir did not choose that path because he had a grand theory about the future of work.</p>



<p class="wp-block-paragraph">The company went remote because he lived in Chile and simply could not find enough people to hire locally.</p>



<p class="wp-block-paragraph">At one point, he actually planned to move to Silicon Valley and start the company there, but then he met his wife and changed his plans.</p>



<p class="wp-block-paragraph">That experience shaped another piece of advice he gives developers who want to become founders: instead of copying someone else’s formula, <strong>start with your own circumstances, abilities, and strengths</strong>:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">I do think you need to work in what kind of environment you have, what kind of skills you have, what kind of strengths you have, and then work from that.</p>
</blockquote>



<p class="wp-block-paragraph">For that reason, founders should not treat his remote model as a template that automatically works for every company. </p>



<p class="wp-block-paragraph">The same goes for following other founders too closely:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">You follow what I have done, or you see something on X or some other founder, and then you just blindly follow them. That&#8217;s probably not going to lead to a very good outcome.</p>
</blockquote>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="538" src="https://shiftmag.dev/wp-content/uploads/2026/10/Amir2-1024x538.png?x32039" alt="" class="wp-image-12957" srcset="https://shiftmag.dev/wp-content/uploads/2026/10/Amir2-1024x538.png 1024w, https://shiftmag.dev/wp-content/uploads/2026/10/Amir2-300x158.png 300w, https://shiftmag.dev/wp-content/uploads/2026/10/Amir2-768x403.png 768w, https://shiftmag.dev/wp-content/uploads/2026/10/Amir2.png 1200w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">After his talk at Shift, Amir and I sat down for an inspiring conversation. Photo by Marko Mudrinić.</figcaption></figure>



<h2 class="wp-block-heading" id="if-you-start-an-ai-startup-today-you-may-already-be-too-late">If you start an AI startup today, you may already be too late</h2>



<p class="wp-block-paragraph">AI has become impossible to ignore, swallowing the tech industry’s attention. <strong>Almost every startup wants to become AI-native</strong>, conversations turn to agents, and everyone looks to Silicon Valley again.</p>



<p class="wp-block-paragraph">So I asked Amir whether developers outside this wave should feel they’re missing out. He gave one of the most direct answers of our conversation:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">In a lot of cases, if you actually start an AI startup right now, you&#8217;re probably too late.</p>
</blockquote>



<p class="wp-block-paragraph">For him, understanding technological S-curves matters much more: <strong>founders need to recognize which stage a particular technology has reached</strong>.</p>



<p class="wp-block-paragraph">Ideally, he says, you enter a trend before exponential growth begins. AI may already be deep into that curve, while space could still be near the start.</p>



<p class="wp-block-paragraph">The reverse is true too: <strong>in mature markets, making a significant impact is much harder</strong>:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">For instance, if you do SaaS right now, that is freaking mature. You have very, very entrenched companies. Of course, maybe by some miracle you can do something that&#8217;s 10x, but it&#8217;s highly unlikely.</p>
</blockquote>



<p class="wp-block-paragraph">Rather than chase the hottest field, founders should understand where the market and their company sit on the S-curve and where they want to compete. The same applies to fundraising: know what game you’re choosing to play.</p>



<h2 class="wp-block-heading" id="before-taking-vc-money-you-need-to-know-which-game-you-are-playing">Before taking VC money, you need to know which game you are playing</h2>



<p class="wp-block-paragraph">Todoist has remained bootstrapped, and when I asked how he thinks about company financing today, Amir returned to the same starting point: founders who lack traction, customers, proof that the product works, or a personal track record will struggle to raise money. </p>



<p class="wp-block-paragraph">His preferred order therefore remains the same. First, build, learn, and find customers. <strong>Only then decide whether fundraising makes sense at all</strong>. </p>



<p class="wp-block-paragraph">But another part of that decision matters even more. VC money comes with very specific expectations about what company success should look like:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">VCs want to see billion-dollar outcomes. If you don&#8217;t have a realistic way to achieve that, even if you lie, say, okay, this is our path, but you don&#8217;t really believe it, at some point they are going to push you to their direction, and then they&#8217;re going to mess up your company, because that is what you&#8217;re signed up for.</p>
</blockquote>



<p class="wp-block-paragraph">The problem, then, does not lie in the VC model itself. The problem appears when a founder accepts that kind of capital without actually wanting to build the kind of company that the VC game requires.</p>



<p class="wp-block-paragraph">That is one of the reasons <strong>Amir has never raised money for Todoist</strong>:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Why I have never raised money for Todoist is because I don&#8217;t want to build a billion-dollar ARR business.</p>
</blockquote>



<h2 class="wp-block-heading" id="bootstrapping-is-like-climbing-everest-without-oxygen">Bootstrapping is like climbing Everest without oxygen</h2>



<p class="wp-block-paragraph">That does not mean Amir sees bootstrapping as an easier or more relaxed alternative to the VC path. </p>



<p class="wp-block-paragraph">I also wanted to know whether people have romanticized bootstrapping in recent years. Founder stories often present it as a way to <strong>avoid investor pressure, keep control, and build a company at your own pace</strong>. </p>



<p class="wp-block-paragraph">Amir dismantles that image:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">But I think bootstrapping is incredibly hard. It&#8217;s not the easy path. What I actually tell my team is: we are climbing Mount Everest without oxygen, and with oxygen is the capital. That is what you&#8217;re signing up for.</p>
</blockquote>



<p class="wp-block-paragraph">So the real question is not whether bootstrapping beats VC funding. Founders need to ask what they actually want from their company and from their own lives. </p>



<p class="wp-block-paragraph">Not every company fits the VC path. At the same time, bootstrapping comes with serious constraints of its own.</p>



<h2 class="wp-block-heading" id="todoist-removed-balance-from-its-core-value">Todoist removed &#8220;balance&#8221; from its core value</h2>



<p class="wp-block-paragraph">Building a company for 20 years doesn’t mean 20 years of growth. Amir recalled that Todoist and Buffer followed a similar path: <strong>years of growth, then a plateau and slight decline</strong>.</p>



<p class="wp-block-paragraph">As he puts it, growth can make you think you’re &#8220;the freaking smartest.&#8221; When it stops, you realize how much needs rethinking. At Todoist, that meant major culture changes, including dropping one word from a core value:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">We actually had this core value called ambition and balance. And then we just removed balance.</p>
</blockquote>



<p class="wp-block-paragraph">Amir says that at one point he became deeply unhappy with how things worked inside his own company. He did not think the company showed enough ambition, he did not like the level of output, and he felt that the team no longer pushed boundaries.</p>



<p class="wp-block-paragraph">The cultural shift went beyond words. During that process, the company let some people go, while others chose to leave:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">And I think right now we have a lot more calibrated company because of that.</p>
</blockquote>



<p class="wp-block-paragraph">He sees an important lesson in that experience for new founders. The culture you create at the beginning, the first people you hire, and the first core values you define can have a huge impact on the company you eventually build. </p>



<p class="wp-block-paragraph"><strong>Once the company grows, changing those foundations becomes much harder</strong>.</p>



<h2 class="wp-block-heading" id="todoist-now-wants-to-become-a-suite-of-apps">Todoist now wants to become a suite of apps</h2>



<p class="wp-block-paragraph">At the end of our conversation, I wanted to know what comes next, and Amir described two very different possible futures for Todoist. </p>



<p class="wp-block-paragraph">In one, he could cut 50 percent of the team, generate much more profit and, as he joked, perhaps buy himself a small yacht, while in the other, <strong>the company would take on a much bigger problem, which is ultimately the path they chose</strong>:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Or we can just say, okay, let&#8217;s freaking solve a much, much bigger problem. So for us, we are going to create a suite of apps.</p>
</blockquote>



<p class="wp-block-paragraph">The first product in that expansion will focus on automations:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Honestly, I think what we have done technologically, but also on product side, will be really, really cool for people to see, because everything is natural language. And you tell what you want to build, and then the system builds it for you. And it&#8217;s quite affordable. So automations will be the first leak. The other one is team communication.</p>
</blockquote>



<p class="wp-block-paragraph">When Amir mentioned team communication, I asked whether that meant a return for Twist, Doist’s asynchronous team communication app, which the company developed as an alternative to real-time workplace chat.</p>



<p class="wp-block-paragraph">Amir replied:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">It&#8217;s coming back in a very strong form.</p>
</blockquote>



<p class="wp-block-paragraph">That fits with the broader direction Amir described for Todoist, as the company now plans to expand beyond its core product and build a wider suite of apps.</p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe loading="lazy" title="Todoist Founder: AI Changes What Great Engineers Do!" width="500" height="281" src="https://www.youtube.com/embed/Kd5S3OdOMAk?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div></figure>


<figure class="wp-block-post-featured-image"><img loading="lazy" decoding="async" width="1200" height="630" src="https://shiftmag.dev/wp-content/uploads/2026/10/amir1.png?x32039" class="attachment-post-thumbnail size-post-thumbnail wp-post-image" alt="" style="object-fit:cover;" srcset="https://shiftmag.dev/wp-content/uploads/2026/10/amir1.png 1200w, https://shiftmag.dev/wp-content/uploads/2026/10/amir1-300x158.png 300w, https://shiftmag.dev/wp-content/uploads/2026/10/amir1-1024x538.png 1024w, https://shiftmag.dev/wp-content/uploads/2026/10/amir1-768x403.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px" /></figure><p>The post <a href="https://shiftmag.dev/todoist-founder-ai-startups-too-late-12943/">Todoist Founder: Developers Should Focus on Solving Customer Problems</a> appeared first on <a href="https://shiftmag.dev">ShiftMag</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>At NVIDIA, &#8220;Senior Software Engineer&#8221; can mean three different levels</title>
		<link>https://shiftmag.dev/nvidia-career-ladder-12717/</link>
		
		<dc:creator><![CDATA[Anastasija Uspenski]]></dc:creator>
		<pubDate>Tue, 20 Oct 2026 13:43:26 +0000</pubDate>
				<category><![CDATA[Career]]></category>
		<category><![CDATA[Software Engineering]]></category>
		<category><![CDATA[career]]></category>
		<category><![CDATA[career ladder]]></category>
		<category><![CDATA[NVDIA]]></category>
		<guid isPermaLink="false">https://shiftmag.dev/?p=12717</guid>

					<description><![CDATA[<p>NVIDIA’s career ladder isn’t linear, and it’s pretty hard to make sense of from the outside. I did some digging and asked around, so you don’t have to.</p>
<p>The post <a href="https://shiftmag.dev/nvidia-career-ladder-12717/">At NVIDIA, &#8220;Senior Software Engineer&#8221; can mean three different levels</a> appeared first on <a href="https://shiftmag.dev">ShiftMag</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">In most large tech companies, an engineer’s career path looks predictable on the surface. You start as a Software Engineer, move up to a Senior role as you gain experience, and then step into Staff and Principal roles if you want to stay on the technical track. If you prefer working with people and organizational structures, you switch to management.</p>



<p class="wp-block-paragraph">Things get more complex at <a href="https://shiftmag.dev/tag/nvidia/" target="_blank" rel="noreferrer noopener">NVIDIA</a>. A job title does not always reveal an engineer’s exact position in the engineering organization, and a level upgrade does not always come with a new title.</p>



<p class="wp-block-paragraph"><strong>NVIDIA uses a deep technical career ladder</strong>. This structure lets engineers reach high internal levels without moving into people management. However, NVIDIA does not publish a simple chart mapping each job title to a specific internal level. Job postings show that single titles overlap across multiple levels. An engineer&#8217;s scope of responsibility, problem complexity, and cross-team impact reflect their true seniority far better than a title.</p>



<p class="wp-block-paragraph">To understand NVIDIA’s engineering career ladder in practice, I reviewed its current job postings and compared the responsibilities and levels listed.</p>



<h2 class="wp-block-heading" id="how-nvidia-measures-seniority">How NVIDIA measures seniority</h2>



<p class="wp-block-paragraph">After reviewing NVIDIA job postings, I found that the Senior Software Engineer title can cover several internal levels. </p>



<p class="wp-block-paragraph">The CUDA UMD-GPU Kernel Scheduling posting lists salary ranges for Levels 3 and 4, while the AI Inference Performance posting lists ranges for Levels 4 and 5. </p>



<p class="wp-block-paragraph">The CUDA posting also notes multiple openings for candidates with different experience levels. Together, these postings show that the title spans at least Levels 3, 4, and 5. Job titles and internal levels are not interchangeable.</p>



<p class="wp-block-paragraph">Public databases like <a href="https://www.levels.fyi/en-gb/companies/nvidia/salaries/software-engineer?country=254" target="_blank" rel="noreferrer noopener">Levels.fyi map NVIDIA&#8217;s structure into a simple series of individual contributor (IC) levels.</a> However, candidates should view those maps as external estimates rather than official company policy.</p>



<p class="wp-block-paragraph">Evaluating how an engineer&#8217;s scope of responsibility expands with each step provides a much clearer picture.</p>



<h2 class="wp-block-heading" id="moving-from-task-execution-to-problem-ownership">Moving from task execution to problem ownership</h2>



<p class="wp-block-paragraph">Early in the career ladder, a Software Engineer must master their core tech stack, implement clearly defined features, and deliver clean, maintainable code. <strong>As experience grows, responsibilities shift</strong>.</p>



<p class="wp-block-paragraph">Managers evaluate engineers on how independently they understand a problem, analyze system constraints, propose solutions, and ship code to production.</p>



<p class="wp-block-paragraph">This shift marks a core transition that repeats across higher ladder levels: detailed assignments decrease, and open-ended problems requiring self-directed solutions increase.</p>



<h2 class="wp-block-heading" id="what-nvidia-expects-from-senior-engineer-beyond-writing-good-code">What NVIDIA expects from senior engineer &#8211; beyond writing good code</h2>



<p class="wp-block-paragraph">At the Senior level, strong technical skills are expected. Engineers must also decide what to build and why.</p>



<p class="wp-block-paragraph"><strong>A Senior Software Engineer </strong>makes technical decisions, evaluate system-wide trade-offs, identify future risks, collaborate across teams, and shape system architecture.</p>



<p class="wp-block-paragraph">NVIDIA&#8217;s job postings I lokeed at highlight these expectations. For example, the Senior Software Engineer (AI Inference Performance) role spans AI models, serving software, distributed runtimes, CUDA kernels, and GPU architecture. Senior engineers lead end-to-end performance analyses, resolve bottlenecks, and collaborate across specialized teams.</p>



<p class="wp-block-paragraph">Similarly, the Senior Software Engineer (CUDA UMD – GPU Kernel Scheduling) role lists core responsibilities such as architecting new features, coordinating multi-team development, and influencing the future CUDA API and programming model.</p>



<p class="wp-block-paragraph">At this level, the difference between two engineers with the same title shows up in <strong>system ownership</strong> rather than job titles.</p>



<p class="wp-block-paragraph">There is another internal detail that makes this distinction particularly interesting. According to a source at NVIDIA I spoke with, engineers generally do not see their colleagues’ IC levels unless they are in management:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">No one sees a colleague’s level outside of management. That gets a bit interesting during performance reviews because someone might be excellent for IC3, but I would expect more from them if they were IC4.</p>
</blockquote>



<p class="wp-block-paragraph">That means two engineers can carry the same visible title while being evaluated against different internal expectations. Someone’s performance might look exceptional at one level but simply meet the bar at the next.</p>



<h2 class="wp-block-heading" id="senior-staff-engineers-work-across-teams">Senior Staff engineers work across teams</h2>



<p class="wp-block-paragraph">The next major jump happens when an engineer&#8217;s impact crosses team boundaries.</p>



<p class="wp-block-paragraph">NVIDIA hires roles such as Senior Staff Software Engineer proving that an advanced layer of IC roles exists between Senior and Principal levels.</p>



<p class="wp-block-paragraph">At this stage, understanding a single service or codebase is not enough. Engineers must manage cross-system dependencies, identify long-term technical risks, and align decisions across teams with competing priorities.</p>



<p class="wp-block-paragraph"><strong>Influence without authority becomes critical at this level</strong>. A Senior Staff Engineer does not manage team members or issue top-down commands. Instead, their technical reputation, logic, and judgment drive alignment across engineering teams.</p>



<p class="wp-block-paragraph">The same NVIDIA source described this progression in much simpler terms:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">The IC level determines whether you have the ability, with or without supervision, to handle a subfeature of something bigger, handle a feature, handle a feature that requires coordination with one team, or handle a feature where you coordinate multiple teams as the lead. And then the holy grail: steering the company and the industry, something like IC7 or IC8. </p>
</blockquote>



<p class="wp-block-paragraph">That description captures the logic behind the ladder more clearly than titles alone. Each step expands the scope of ownership, from a contained piece of work to increasingly complex technical efforts that span teams and, at the highest levels, influence the direction of the company and the wider industry. </p>



<p class="wp-block-paragraph">As engineers move up the technical ladder, their role shifts from individual task execution to <strong>creating conditions that help large teams make better technical decisions</strong>.</p>



<h2 class="wp-block-heading" id="principal-engineers-plan-strategy-years-in-advance">Principal Engineers plan strategy years in advance</h2>



<p class="wp-block-paragraph">At the Principal level, engineers<strong> help shape technical direction across the company</strong>. They improve existing architectures, plan future systems, identify organization-wide bottlenecks, and lead major changes from strategy and design through implementation and deployment. In infrastructure roles, this can include setting a multi-year vision for automation and configuration management.</p>



<p class="wp-block-paragraph">They remain close to the technology, though they may spend less time writing production code. Their work can include prototyping, reviewing critical code, solving scaling problems, and weighing architectural trade-offs.</p>



<p class="wp-block-paragraph">In compute infrastructure, a Principal Engineer may guide the design of a global platform with thousands of nodes and tens of thousands of virtual machines and containers. They may also help build the operational foundation for advanced AI models.</p>



<p class="wp-block-paragraph">Their impact comes less from the amount of code they write and more from <strong>guiding important technical decisions while staying close enough to the technology to make those decisions well</strong>.</p>



<h3 class="wp-block-heading" id="distinguished-engineers-combine-deep-expertise-with-company-wide-influence">Distinguished Engineers combine deep expertise with company-wide influence</h3>



<p class="wp-block-paragraph">Above the Principal level, NVIDIA places Distinguished Engineers. At this stage, impact extends across entire business units, platforms, and technology strategies.</p>



<p class="wp-block-paragraph">For example, the current job posting for this role requires <strong>at least 18 years of relevant experience</strong> and a proven record of company-wide technical leadership at the Principal or Distinguished level.</p>



<p class="wp-block-paragraph">Distinguished Engineers improve system reliability, scalability, and performance by setting technical standards and reviewing architecture. They work across platform, hardware, infrastructure, and service teams to turn recurring problems into lasting software improvements.</p>



<p class="wp-block-paragraph">The role combines deep technical expertise with influence across the organization. They spot important issues early, set technical direction, and help teams act on it.</p>



<h2 class="wp-block-heading" id="career-growth-means-more-responsibility-and-technical-impact">Career growth means more responsibility and technical impact</h2>



<p class="wp-block-paragraph">Removing job titles reveals a clear career trajectory:</p>



<ul class="wp-block-list">
<li><strong>Early Career</strong>: Delivering reliable work and completing assigned tasks.</li>



<li><strong>Mid-Level</strong>: Solving complex problems independently without pre-defined blueprints.</li>
</ul>



<ul class="wp-block-list">
<li><strong>Senior &amp; Staff</strong>: Shaping large systems, coordinating technical strategy across teams, and forecasting long-term trade-offs.</li>



<li><strong>Principal &amp; Distinguished</strong>: Setting technical strategy and guiding large engineering teams without direct managerial authority.</li>
</ul>



<p class="wp-block-paragraph">A Principal Engineer is not just a Senior Engineer who knows more programming languages or GPU architecture. A Distinguished Engineer is not just a Principal Engineer with more experience. Each role brings broader responsibilities and a longer-term view, including thinking about the architectures the company will need in three to five years.</p>



<h2 class="wp-block-heading" id="nvidia-engineers-can-grow-their-careers-without-becoming-managers">NVIDIA engineers can grow their careers without becoming managers</h2>



<p class="wp-block-paragraph">In some organizations, engineers move into management to grow their influence and compensation. <strong>Technical career ladders offer another path</strong>: Principal and Distinguished Engineers can shape company decisions while continuing to focus on technology.</p>



<p class="wp-block-paragraph">This technical path fits NVIDIA&#8217;s mission. The company builds GPU architectures, CUDA software, AI infrastructure, networking systems, autonomous platforms, and large-scale distributed systems. Solving these engineering challenges requires <strong>deep technical ownership that reaches beyond individual teams or products</strong>.</p>



<p class="wp-block-paragraph">At NVIDIA, seniority reflects an engineer&#8217;s ability to spot critical engineering challenges, map out technical strategies, and help teams deliver effective solutions.</p>



<p class="wp-block-paragraph">A top engineer looks beyond today’s questions to help the company solve tomorrow’s biggest technical challenges.</p>



<p class="wp-block-paragraph"></p>


<figure class="wp-block-post-featured-image"><img loading="lazy" decoding="async" width="1200" height="630" src="https://shiftmag.dev/wp-content/uploads/2026/10/nvidia.naslovna.nova_.png?x32039" class="attachment-post-thumbnail size-post-thumbnail wp-post-image" alt="" style="object-fit:cover;" srcset="https://shiftmag.dev/wp-content/uploads/2026/10/nvidia.naslovna.nova_.png 1200w, https://shiftmag.dev/wp-content/uploads/2026/10/nvidia.naslovna.nova_-300x158.png 300w, https://shiftmag.dev/wp-content/uploads/2026/10/nvidia.naslovna.nova_-768x403.png 768w, https://shiftmag.dev/wp-content/uploads/2026/10/nvidia.naslovna.nova_-1024x538.png 1024w" sizes="auto, (max-width: 1200px) 100vw, 1200px" /></figure>


<p class="wp-block-paragraph"></p>
<p>The post <a href="https://shiftmag.dev/nvidia-career-ladder-12717/">At NVIDIA, &#8220;Senior Software Engineer&#8221; can mean three different levels</a> appeared first on <a href="https://shiftmag.dev">ShiftMag</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Chris Davidson, Atlassian: &#8220;Ask AI to challenge you&#8221;</title>
		<link>https://shiftmag.dev/chris-davidson-atlassian-ask-ai-to-challenge-you-12526/</link>
		
		<dc:creator><![CDATA[Ivan Simic]]></dc:creator>
		<pubDate>Fri, 16 Oct 2026 11:18:06 +0000</pubDate>
				<category><![CDATA[Artificial Intelligence]]></category>
		<category><![CDATA[Video]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[Atlassian]]></category>
		<category><![CDATA[chris davidson]]></category>
		<category><![CDATA[Developer Productivity]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[Infobip Shift 2026]]></category>
		<category><![CDATA[Shift Conference]]></category>
		<guid isPermaLink="false">https://shiftmag.dev/?p=12526</guid>

					<description><![CDATA[<p>As a Solutions Engineer at Atlassian, Chris Davidson works with clients in an effort to make the most of Atlassian's tools and has first-hand experience of how we adapt to using AI.</p>
<p>The post <a href="https://shiftmag.dev/chris-davidson-atlassian-ask-ai-to-challenge-you-12526/">Chris Davidson, Atlassian: &#8220;Ask AI to challenge you&#8221;</a> appeared first on <a href="https://shiftmag.dev">ShiftMag</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">During <a href="https://shiftmag.dev/tag/infobip-shift-2026/" target="_blank" rel="noreferrer noopener">this year&#8217;s Infobip Shift</a> in Zadar, Croatia, Chris spoke to us about AI tools and gave us some inside information on <strong>how Atlassian</strong>, the creators of Jira and Confluence, <strong>work with AI internally</strong>. With his role considering lots of work with clients, he also had things to say about how we use (or try to use) AI tools efficiently.</p>



<p class="wp-block-paragraph">Chris has seen his share of enthusiastic implementations that don&#8217;t always work out. Funnily enough, it&#8217;s not always the clients and partners that get carried away with AI; sometimes it&#8217;s Atlassian. Chris noted that the company is just like many others, trying to <strong>figure out the impact of AI as they go on</strong>. The one thing to point out is the usage of agents:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">I think about agents inside Atlassian, and as a team member adopting them, it&#8217;s really interesting to see how quickly they get spun up, but then how quickly they only have only one or two uses. </p>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">Then you even get to the point where there&#8217;s almost like a clean up process. Many agents, duplicated names and activities, and you have to clean those up. You almost need agents to clean up agents.</p>
</blockquote>



<p class="wp-block-paragraph">Chris told us that, in Atlassian, they&#8217;re trying to move fast and react to what customers need and want. This often results in making services and face challenges in duplication. </p>



<h2 class="wp-block-heading" id="dont-use-ai-selfishly">Don&#8217;t use AI selfishly</h2>



<p class="wp-block-paragraph">Despite AI agents and tools&#8217; ability to understand almost everything about software, building things with AI is something most of us do by ourselves. We create tools and use AI to write code we&#8217;re working on, but rarely we go and seek how we could work with others. You could argue that using AI has become a bit of a selfish thing; we&#8217;re all trying to see what we can do on our own. Chris told us that teams that legitimately change the way they work with AI <strong>are usually the ones who use it to collaborate with others.</strong></p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Maybe they&#8217;re opening up more of an interface that&#8217;s like self-serve to another part of the organization. You know, you think like QA management and test and development, that interfance, how can we make that better?</p>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">It&#8217;s not about what we&#8217;re coding in our IDE, it&#8217;s more about how that work is then moving to the next part of the organization. So I really look for where are connections across that whole development lifecycle, not just the coding bit.</p>
</blockquote>



<h2 class="wp-block-heading" id="ai-is-magnifying-some-challenges">AI is magnifying some challenges</h2>



<p class="wp-block-paragraph">An interesting trend in software development today is adding AI for the sake of AI, thus adding more steps in an otherwise very streamlined process. Chris said that we, as humans, need to be very intentional in finding steps where AI can help. We should not just<strong> try and apply AI to every single part</strong> of every development lifecycle. Doing that just causes something Chris calls &#8220;drag&#8221;:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Where it causes drag is when poor quality comes down the pipe. We&#8217;ve known for years that it costs a lot more to fix poor quality later in the lifecycle than earlier, and I just think we&#8217;re magnifying that challenge with AI because it&#8217;s so convincing. </p>
</blockquote>



<p class="wp-block-paragraph">Humans can be a little bit lazy sometimes, said Chris, and added that we might let a lot of things pass through unintentionally. Those things get further down the pipeline and<strong> AI then causes a real drag</strong> because it&#8217;s harder to find and fix, and it takes more effort from everyone. </p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe loading="lazy" title="Why Teams Quietly Quit Their AI Tools?" width="500" height="281" src="https://www.youtube.com/embed/U2izoZODpkI?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div></figure>



<h2 class="wp-block-heading" id="its-challenging-to-lead-in-the-ai-era">It&#8217;s challenging to lead in the AI era</h2>



<p class="wp-block-paragraph">Engineering leaders today, such as higher-level managers and executives, do not have it easy today. Chris echoed this statement, saying that he &#8220;feels for engineering leaders at the moment&#8221;, and adding that there&#8217;s a whole other layer of dealing with both the technical and human challenges of this AI era. </p>



<p class="wp-block-paragraph">In terms of where leaders go wrong, Chris used a <strong>high reliance on data as an example. </strong>In his opinion, with more data of all sorts available today, leaders tend to rotate more towards it than towards qualitative metrics:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Managers can defend and they can articulate well with data. There is a risk that you over-rotate purely to data and not those qualitative metrics. So, engineering managers need to also keep qualitative metrics, developer surveys and developer pulses.</p>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">That&#8217;s how you stay close to what the feeling is on the ground, from both a performance perspective but also a human perspective.</p>
</blockquote>



<h2 class="wp-block-heading" id="ask-your-ai-to-challenge-you">Ask your AI to challenge you</h2>



<p class="wp-block-paragraph">Using AI tools without being skeptical and critical of them might be the single worst trend not just in software development today, but in almost every industry (and in personal lives of many). Chris noted that it&#8217;s really dangerous how convincingly wrong AI tools and agents might be, and without knowing how to properly work with them, you might be a victim of their hallucinations without ever noticing it. Chris explained:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">A practice I see many teams do is you use AI to challenge you. You have to have a viewpoint and a hypothesis about what you&#8217;re doing and why you&#8217;re doing it and how you&#8217;re approaching it. Then you use AI to challenge that. </p>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">Guaranteed, it will come up with things you didn&#8217;t think of. It will come up with elements of code or something in the code that you wouldn&#8217;t have seen, and it would not have been detected had it not been for AI.</p>
</blockquote>



<h2 class="wp-block-heading" id="ask-how-you-could-use-ai-to-help-other-teams">Ask how you could use AI to help other teams</h2>



<p class="wp-block-paragraph">Wrapping up our chat with Chris, we asked him how would he advise those working with AI every day to make the most of their experience, and collaboration with AI tools:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">A practical change might be to take some time out of your week, draw a circle and put all the teams that you work with around that circle, and then start to pull your top three or five teams that are critical to that flow of work, and ask yourself, how could we be using AI better in our relationship with those teams?</p>
</blockquote>



<p class="wp-block-paragraph">If you switch your thought process related to AI from thinking how can the technology make your job easier to how it can make working with others easier, it <strong>might just be the thing you need</strong> to get the edge. Just don&#8217;t go using it without questioning the things it says. </p>


<figure class="wp-block-post-featured-image"><img loading="lazy" decoding="async" width="2100" height="1400" src="https://shiftmag.dev/wp-content/uploads/2026/10/Chris_Davidson_final-scaled.jpg?x32039" class="attachment-post-thumbnail size-post-thumbnail wp-post-image" alt="Chris Davidson, Atlassian, on Infobip Shift 2026" style="object-fit:cover;" srcset="https://shiftmag.dev/wp-content/uploads/2026/10/Chris_Davidson_final-scaled.jpg 2100w, https://shiftmag.dev/wp-content/uploads/2026/10/Chris_Davidson_final-300x200.jpg 300w, https://shiftmag.dev/wp-content/uploads/2026/10/Chris_Davidson_final-1024x683.jpg 1024w, https://shiftmag.dev/wp-content/uploads/2026/10/Chris_Davidson_final-768x512.jpg 768w" sizes="auto, (max-width: 2100px) 100vw, 2100px" /></figure><p>The post <a href="https://shiftmag.dev/chris-davidson-atlassian-ask-ai-to-challenge-you-12526/">Chris Davidson, Atlassian: &#8220;Ask AI to challenge you&#8221;</a> appeared first on <a href="https://shiftmag.dev">ShiftMag</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>I Talk to Production Every Morning. It Talks Back.</title>
		<link>https://shiftmag.dev/i-talk-to-production-every-morning-it-talks-back-12679/</link>
		
		<dc:creator><![CDATA[Hrvoje Rancic]]></dc:creator>
		<pubDate>Thu, 15 Oct 2026 13:22:52 +0000</pubDate>
				<category><![CDATA[Artificial Intelligence]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[artificial intelligence]]></category>
		<category><![CDATA[Developer Productivity]]></category>
		<category><![CDATA[Engineer Explains]]></category>
		<guid isPermaLink="false">https://shiftmag.dev/?p=12679</guid>

					<description><![CDATA[<p>A lot of people are leaving software engineering because agent-based workflows killed their joy. I don't have that problem; I never liked the job in the first place.</p>
<p>The post <a href="https://shiftmag.dev/i-talk-to-production-every-morning-it-talks-back-12679/">I Talk to Production Every Morning. It Talks Back.</a> appeared first on <a href="https://shiftmag.dev">ShiftMag</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">I was good at math and algorithm coding challenges in high school, and I knew I would rather not be poor, if possible, and the salary of a software engineer is decent. I started working with a naive assumption that soon I will know how to do the job well, it would become a routine and then I would just cruise through it. </p>



<p class="wp-block-paragraph">I soon learned that <strong>college did not prepare me for the job</strong>, got some kind of idea of the skills and knowledge required to ship production software, and figured out that the more I knew the less stressful the job would be.</p>



<p class="wp-block-paragraph">So over the course of many years, much against my will, <strong>I became an expert</strong>. Then AI came into the picture, and nothing fundamentally changed, because the job of a software engineer is to help achieve business goals by leveraging their understanding of the technology, and now there was yet another technology to learn.</p>



<h2 class="wp-block-heading" id="get-ahead-of-security-issues">Get ahead of security issues</h2>



<p class="wp-block-paragraph">I have decided to <strong>focus on making the job less miserable for me</strong>. My original approach, once I learned how to use tools such as Claude, was to do the same amount of work as before and then just chill.</p>



<p class="wp-block-paragraph">Nobody ever complained about my performance, so I figured if I just did the same amount of stuff in less time, I would have time for the things I enjoyed, like drinking coffee with other slackers on the balcony or writing pieces <a href="https://shiftmag.dev/postmortem-the-boiler-incident-6986/" target="_blank" rel="noreferrer noopener">such as this one</a>.</p>



<p class="wp-block-paragraph">This felt good for a while, but still there were annoying things that needed to be done. One of them is upgrading packages to mitigate security issue. I don’t know about you, but I fall asleep each time I need to read a <a href="https://en.wikipedia.org/wiki/Common_Vulnerabilities_and_Exposures" target="_blank" rel="noreferrer noopener">CVE Record</a>.</p>



<p class="wp-block-paragraph">So I get AI to write me a short horror story written from the perspective of a sarcastic and frustrated application security engineer whose warnings companies have ignored at their own peril. </p>



<p class="wp-block-paragraph">The story always has some snippets that <strong>help me understand how this exploit might happen on services I am responsible for</strong>, and it always ends up with my company going bankrupt, this really makes the lessons stick and motivates me to fix the issues.</p>



<p class="wp-block-paragraph">Sure I could completely automate the bumping, but where is fun in that, and what do I learn?</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="538" src="https://shiftmag.dev/wp-content/uploads/2026/10/ShiftMag-article-banner-3-1-1024x538.png?x32039" alt="" class="wp-image-12706" srcset="https://shiftmag.dev/wp-content/uploads/2026/10/ShiftMag-article-banner-3-1-1024x538.png 1024w, https://shiftmag.dev/wp-content/uploads/2026/10/ShiftMag-article-banner-3-1-300x158.png 300w, https://shiftmag.dev/wp-content/uploads/2026/10/ShiftMag-article-banner-3-1-768x403.png 768w, https://shiftmag.dev/wp-content/uploads/2026/10/ShiftMag-article-banner-3-1.png 1200w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h2 class="wp-block-heading" id="the-production-can-be-cruel">The production can be cruel</h2>



<p class="wp-block-paragraph">The other thing I don’t like is the production; <strong>the production is cruel</strong>. You surround your code with all kinds of tests, you create the production like environment with simulated traffic, you use feature flags, you deploy small increments often, and still production finds the ways to surprise you, and it has a tendency not to do that during business hours.</p>



<p class="wp-block-paragraph">So you do what you must, add a bunch of observability, create runbooks and alerts. Now, with all that telemetry in place and a coding agent I can finally engage with production properly: not reactively when something goes wrong, but every day <strong>I can chat with the production</strong>, ask it about its concerns, about what is going on lately, if something feels wrong.</p>



<p class="wp-block-paragraph">I do this by invoking the skill that gathers all the data and then reports it with a certain tone of voice. And I’ll tell you one thing, gaze long enough into production telemetry and the production gazes back.</p>



<h2 class="wp-block-heading" id="chat-instead-of-jot">Chat instead of jot</h2>



<p class="wp-block-paragraph">One last example, and I envy the juniors for having the ability to do this from the start, is t<strong>he change in the way how I learn stuff</strong>. When I was starting, I used to jot down the things and concepts I did not understand, I would look them up in articles and books, I would go through half-baked internet tutorials, blog posts and man pages, and still come out with questionable understanding.</p>



<p class="wp-block-paragraph">Now I start a chat session with an AI model, give him a personality, most often it is an old and grumpy but well-mannered distinguished engineer who explains topics with a lot of examples and then asks challenging questions while throwing casual insults, he asks me a challenging question and then roasts my answer before proceeding with the next lesson.</p>



<p class="wp-block-paragraph">Hope you find some of these ideas useful, because I feel much of the talk has been on how to use AI to speed things up, and too little of the talk is about how to use AI to make software more reliable or the process of building and maintaining it more enjoyable.</p>


<figure class="wp-block-post-featured-image"><img loading="lazy" decoding="async" width="1200" height="630" src="https://shiftmag.dev/wp-content/uploads/2026/10/ShiftMag-article-banner-2.png?x32039" class="attachment-post-thumbnail size-post-thumbnail wp-post-image" alt="" style="object-fit:cover;" srcset="https://shiftmag.dev/wp-content/uploads/2026/10/ShiftMag-article-banner-2.png 1200w, https://shiftmag.dev/wp-content/uploads/2026/10/ShiftMag-article-banner-2-300x158.png 300w, https://shiftmag.dev/wp-content/uploads/2026/10/ShiftMag-article-banner-2-1024x538.png 1024w, https://shiftmag.dev/wp-content/uploads/2026/10/ShiftMag-article-banner-2-768x403.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px" /></figure><p>The post <a href="https://shiftmag.dev/i-talk-to-production-every-morning-it-talks-back-12679/">I Talk to Production Every Morning. It Talks Back.</a> appeared first on <a href="https://shiftmag.dev">ShiftMag</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Why Early Releases Often Fail in Multi-Team Organizations</title>
		<link>https://shiftmag.dev/why-early-releases-often-fail-in-multi-team-organizations-12368/</link>
		
		<dc:creator><![CDATA[Vedran Petrović]]></dc:creator>
		<pubDate>Tue, 13 Oct 2026 11:12:07 +0000</pubDate>
				<category><![CDATA[Engineering Management]]></category>
		<category><![CDATA[Product Management]]></category>
		<category><![CDATA[Developer Productivity]]></category>
		<category><![CDATA[early release]]></category>
		<guid isPermaLink="false">https://shiftmag.dev/?p=12368</guid>

					<description><![CDATA[<p>It all starts with one simple question from the business side: "Can we ship this early?"</p>
<p>The post <a href="https://shiftmag.dev/why-early-releases-often-fail-in-multi-team-organizations-12368/">Why Early Releases Often Fail in Multi-Team Organizations</a> appeared first on <a href="https://shiftmag.dev">ShiftMag</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">At best, it means: </p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">What&#8217;s the earliest reduced version we can put in front of real customers to validate against something we actually want to learn?</p>
</blockquote>



<p class="wp-block-paragraph">At worst, it’s just a <strong>date and an announcement</strong> with nothing clearly scoped behind it, so that gap needs to be closed before anyone starts building toward it.</p>



<p class="wp-block-paragraph">That’s where things usually start to go wrong. </p>



<h2 class="wp-block-heading" id="business-product-and-engineering-often-mean-different-things-by-early-release">Business, product, and engineering often mean different things by &#8220;early release&#8221;</h2>



<p class="wp-block-paragraph">Once one customer flow is split across several teams, an early release becomes<strong> real coordination work</strong>. </p>



<p class="wp-block-paragraph">Someone has to lead it end to end, and that usually falls to no one. The reduced scope drifts and nobody is coordinating the whole release.&nbsp;With each new team that holds a certain piece of the project, coordinating them gets harder. The fix is good management, and it requires someone to actually lead the release.</p>



<p class="wp-block-paragraph">Part of the trouble is that the <strong>same words mean different things to different people </strong>who are, of course, the part of the same meeting. When the business asks for an early release, it usually means a reduced scope of something, made to be more of a barebones product than a full-sized one. This is something that’s enough to validate assumptions and deliver value while the rest is still being built.&nbsp;</p>



<p class="wp-block-paragraph">To most engineers, &#8220;done&#8221; means complete, so &#8220;early release&#8221; gets heard as &#8220;the whole thing, but fast.&#8221; Put an MVP, the smallest version that tests your least-sure assumption in the mix alongside full GA, the finished product for everyone, and one phrase is now pointing at three different targets.</p>



<p class="wp-block-paragraph">Nobody draws the line between them, so scope drifts toward GA by default: <strong>a team builds one target while the plan assumed another</strong>, and that drift is what sinks these releases.&nbsp;</p>



<h2 class="wp-block-heading" id="how-one-customer-journey-becomes-nobodys-problem">How one customer journey becomes nobody&#8217;s problem&nbsp;</h2>



<p class="wp-block-paragraph">Multi-team releases break along a predictable line, and it was mapped a long time ago. In 1968, Melvin Conway published a paper called <em><a href="https://www.melconway.com/Home/pdf/committees.pdf" target="_blank" rel="noreferrer noopener">How Do Committees Invent?</a></em> with an observation that later became known as <a href="https://en.wikipedia.org/wiki/Conway%27s_law" target="_blank" rel="noreferrer noopener">Conway&#8217;s Law</a>. The law states that organizations that design systems are constrained to <strong>produce designs that copy their own communication structure</strong>.&nbsp;&nbsp;</p>



<p class="wp-block-paragraph">Applied to a release, it predicts the break before anyone writes code: split one customer journey across five teams that mostly talk within their own walls, and the software inherits the same boundaries the organization has. The release breaks at the boundary between two teams, where the calls that need the whole picture don&#8217;t belong to anyone.</p>



<p class="wp-block-paragraph">Put that prediction next to a real feature. I kept it deliberately small here, just to picture the shape; <strong>the real ones are bigger and messier.&nbsp;&nbsp;</strong></p>



<p class="wp-block-paragraph">A customer sends across dozens of destinations, each with its own registration rules. Provisioning sets the customer up to send, sender registration gets each sender approved per destination. The hard cases raise questions neither team can answer alone: when a sender is fine in one country and refused in the next, does provisioning collect a fallback, does registration substitute one, or does the campaign ship partial?&nbsp;&nbsp;</p>



<h2 class="wp-block-heading" id="no-owners-means-no-decisions">No owners means no decisions</h2>



<p class="wp-block-paragraph">Since there&#8217;s no owner, there is no decision. </p>



<p class="wp-block-paragraph">Instead, <strong>each team codes to its own assumption and passes its own tests</strong>, and the assumptions only collide in some cases that cross between teams. The first customer with a mixed set of destinations goes live approved in some and silently refused in others. This is a fix that looks obvious at this size, but at real scale it&#8217;s dozens of these calls across five teams, and the <em>someone</em> who should make them is still <em>no one</em>.&nbsp;</p>



<p class="wp-block-paragraph">It starts when the date is set before the scope or target customer is clear. Then teams start building without knowing where to stop. One team works on an edge case for full GA, while another builds for customers who aren’t onboarded yet.</p>



<p class="wp-block-paragraph">&#8220;Early release&#8221; becomes &#8220;the whole thing&#8221; slowly, one decision at a time. It stays invisible because each team is doing good work on its own. <strong>The drift only exists at the level nobody is watching</strong>, which is the whole end-to-end journey, or the <em>big picture</em>. A release that goes out with no defined scope, no chosen customer, and no one leading it pushes the risk onto whoever is downstream.</p>



<p class="wp-block-paragraph">Cutting scope on purpose is different. You choose what to leave out and handle the rest manually, so the customer still gets value without needing the unfinished parts.</p>



<h2 class="wp-block-heading" id="the-case-against-shared-ownership">The case against shared ownership</h2>



<p class="wp-block-paragraph">Shared ownership across those teams is where the whole falls through. Everybody owns a piece, which really means no one owns the whole. </p>



<p class="wp-block-paragraph"><strong>When multiple teams each hold a piece of a customer journey, someone has to lead the end-to-end release.</strong> This should ideally be one or two people who know the flow in real detail: which service does what, where the boundaries between teams are, what a real customer actually experiences from first call to final result. &nbsp;</p>



<p class="wp-block-paragraph">A steering committee can set priorities and handle escalations, but it can’t lead the release day to day. A manager who only sees team status reports can’t do that either. The details that decide the release depend on how the pieces fit together, and neither of them is close enough to manage that.</p>



<p class="wp-block-paragraph">In practice, <strong>that leadership is coordination work, not a box on the chart</strong>. The owner learns the whole flow and keeps it in their head. They draw the reduced-scope line and publish it, so every team is working from the same picture of what &#8220;first version&#8221; means. &nbsp;</p>



<p class="wp-block-paragraph">They watch for symptoms of a team drifting, which can include&nbsp;hardening a GA edge case or generalizing too early in the process. They decide where &#8220;reduced scope&#8221; ends and &#8220;next phase&#8221; begins and call the release. Most orgs already have someone near this role. Often, that’s a <strong>product manager or a TPM</strong>.&nbsp;</p>



<h2 class="wp-block-heading" id="try-making-one-person-responsible-for-the-release-end-to-end">Try making one person responsible for the release end to end</h2>



<p class="wp-block-paragraph">Define the reduced scope and target customer before you set the date. Write it down. Give it one owner who understands the whole flow. And when the first customer hits a gap, fix it in the shared path so the next customer doesn’t run into it too.</p>



<p class="wp-block-paragraph">The reason it doesn&#8217;t happen is that leading five teams end to end is hard, unglamorous management work, and in most organizations it isn&#8217;t any single manager&#8217;s job. Each manager runs one team and looks after that team&#8217;s part of the release. <strong>The coordination across teams is no one&#8217;s job</strong>.</p>



<p class="wp-block-paragraph">Some organizations avoid this because leading the whole is already built into someone&#8217;s job, or the structure is flat enough that the whole stays visible. The teams aren&#8217;t weak and nobody is being lazy; the job that needs doing simply isn&#8217;t anyone&#8217;s job. The fix is to make one person responsible for the release end to end, and to give them the room to actually lead it.&nbsp;</p>



<p class="wp-block-paragraph">Sometimes, though, one person genuinely can&#8217;t lead the whole thing, however skilled they are. In a matrix organization, an agency where the client signs off every step, or a regulated field where the approval path is set from outside, the flow crosses boundaries a single coordinator can&#8217;t reach across.&nbsp;</p>



<p class="wp-block-paragraph">If your structure genuinely prevents one person from leading end to end, no release process will fix it, because the real constraint is how the organization is wired. The release is only where that constraint <strong>first becomes visible.&nbsp;</strong></p>


<figure class="wp-block-post-featured-image"><img loading="lazy" decoding="async" width="1200" height="630" src="https://shiftmag.dev/wp-content/uploads/2026/10/ShiftMag-article-banner-1.png?x32039" class="attachment-post-thumbnail size-post-thumbnail wp-post-image" alt="" style="object-fit:cover;" srcset="https://shiftmag.dev/wp-content/uploads/2026/10/ShiftMag-article-banner-1.png 1200w, https://shiftmag.dev/wp-content/uploads/2026/10/ShiftMag-article-banner-1-300x158.png 300w, https://shiftmag.dev/wp-content/uploads/2026/10/ShiftMag-article-banner-1-1024x538.png 1024w, https://shiftmag.dev/wp-content/uploads/2026/10/ShiftMag-article-banner-1-768x403.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px" /></figure><p>The post <a href="https://shiftmag.dev/why-early-releases-often-fail-in-multi-team-organizations-12368/">Why Early Releases Often Fail in Multi-Team Organizations</a> appeared first on <a href="https://shiftmag.dev">ShiftMag</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Netflix Engineering Levels Explained: L3 to L7 Career Ladder, Scope, and Pay</title>
		<link>https://shiftmag.dev/netflix-engineering-levels-explained-l3-to-l7-career-ladder-scope-and-pay-12572/</link>
		
		<dc:creator><![CDATA[Marko Crnjanski]]></dc:creator>
		<pubDate>Mon, 12 Oct 2026 12:01:46 +0000</pubDate>
				<category><![CDATA[Career]]></category>
		<category><![CDATA[Developer Experience]]></category>
		<category><![CDATA[career leader]]></category>
		<guid isPermaLink="false">https://shiftmag.dev/?p=12572</guid>

					<description><![CDATA[<p>Want to become a Netflix software engineer? Here’s an overview of Netflix’s engineering career ladder, from entry-level L3 to Software Engineer L7.</p>
<p>The post <a href="https://shiftmag.dev/netflix-engineering-levels-explained-l3-to-l7-career-ladder-scope-and-pay-12572/">Netflix Engineering Levels Explained: L3 to L7 Career Ladder, Scope, and Pay</a> appeared first on <a href="https://shiftmag.dev">ShiftMag</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Netflix&#8217;s public engineering job titles include Software Engineer 3, Software Engineer 4, Senior Software Engineer (L5), Staff Software Engineer (L6), and Software Engineer L7. </p>



<p class="wp-block-paragraph">This article looks at those five individual-contributor levels. It uses <strong>Netflix job descriptions</strong>, t<strong>he company’s culture and compensation policies</strong>, and <strong>reported pay data from <a href="https://www.levels.fyi/?tab=levels" target="_blank" rel="noreferrer noopener">Levels.fyi </a></strong>to show the kind of work associated with each level. Those hiring examples should not be treated as Netflix’s internal promotion criteria.</p>



<h1 class="wp-block-heading" id="engineering-levels-and-compensation">Engineering levels and compensation</h1>



<p class="wp-block-paragraph">Levels.fyi labels L3 through L7 as Engineer, Engineer II, Senior Engineer, Staff Engineer, and Principal Engineer. Netflix&#8217;s recruiting titles vary, sometimes using only a number. The examples below describe particular advertised roles; <strong>they are not a uniform specification for every engineer at that level</strong>.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="256" src="https://shiftmag.dev/wp-content/uploads/2026/10/Screenshot-2026-10-09-at-12.51.31-1024x256.png?x32039" alt="" class="wp-image-12583" srcset="https://shiftmag.dev/wp-content/uploads/2026/10/Screenshot-2026-10-09-at-12.51.31-1024x256.png 1024w, https://shiftmag.dev/wp-content/uploads/2026/10/Screenshot-2026-10-09-at-12.51.31-300x75.png 300w, https://shiftmag.dev/wp-content/uploads/2026/10/Screenshot-2026-10-09-at-12.51.31-767x192.png 767w, https://shiftmag.dev/wp-content/uploads/2026/10/Screenshot-2026-10-09-at-12.51.31.png 1147w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">Based on the last information checked on October 9, 2026, the individual US level pages on Levels.fyi displayed average annual total compensation of approximately <strong>$214K at L3, $321K at L4, $509K at L5, $704K at L6, and $1.14M at L7</strong>. </p>



<p class="wp-block-paragraph">Separately, the company overview reported a median of $430K across US software engineer submissions. What should be noted is these are reported compensation figures, not Netflix&#8217;s official pay bands or a representative census of its employees.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="259" src="https://shiftmag.dev/wp-content/uploads/2026/10/Screenshot-2026-10-09-at-12.51.18-1024x259.png?x32039" alt="" class="wp-image-12584" srcset="https://shiftmag.dev/wp-content/uploads/2026/10/Screenshot-2026-10-09-at-12.51.18-1024x259.png 1024w, https://shiftmag.dev/wp-content/uploads/2026/10/Screenshot-2026-10-09-at-12.51.18-300x76.png 300w, https://shiftmag.dev/wp-content/uploads/2026/10/Screenshot-2026-10-09-at-12.51.18-766x194.png 766w, https://shiftmag.dev/wp-content/uploads/2026/10/Screenshot-2026-10-09-at-12.51.18.png 1145w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">Netflix&#8217;s engineering listings generally describe annual compensation without a separate bonus. Its <em><a href="https://jobs.netflix.com/work-life-philosophy" target="_blank" rel="noreferrer noopener">Work Life Philosophy</a></em> says eligible employees <strong>choose each year how much compensation to receive as salary or stock options</strong>. Under its Personal Top of Market approach, the company assesses an individual&#8217;s market value rather than defining the market through fixed bands and grades. The advertised ranges still depend on the role and can vary by location.</p>



<p class="wp-block-paragraph">For example, the L4 DevOps listing specifies $250K to $413K, and the L6 Developer Productivity listing specifies $600K to $1.066M. The published L7 Growth Engineering description gives a range of $240K to $1.2M. Such overlapping ranges are a reason to check the specific role and offer: <strong>they cannot be used as a standard salary schedule for L4, L6, or L7</strong>.</p>



<h1 class="wp-block-heading" id="l3-and-l4-experience-requirements-and-project-ownership">L3 and L4 Experience requirements and project ownership</h1>



<p class="wp-block-paragraph">Netflix has a dedicated US <a href="https://jobs.netflix.com/careers/new-grads" target="_blank" rel="noreferrer noopener">new-graduate program</a> that includes technical roles, onboarding, and training. Its careers page says new-graduate openings typically appear in September or October, depending on hiring needs. Levels.fyi identifies L3 as entry level, but that <strong>does not mean every Software Engineer 3 vacancy is suitable for someone straight out of university</strong>.</p>



<p class="wp-block-paragraph">The Software Engineer 3 role at Ink, for example, asks for three to five years of software engineering experience, preferably in animation production. It involves working with senior colleagues on tools and platforms for AI-assisted production. That is a specific hiring requirement for a specialist role, not a universal experience threshold for L3.</p>



<p class="wp-block-paragraph">Speaking about L4, the DevOps and CI/CD posting asks for at least four years of professional experience or equivalent practical experience. It describes work on defined parts of larger platform initiatives, with guidance from senior engineers and a manager. An Android L4 posting expects ownership from project scoping through delivery, while still stressing the importance of team support.</p>



<p class="wp-block-paragraph">Together, those examples suggest <strong>increasing independence at L4, with the amount of guidance depending on the role</strong>. Project ownership and support from more experienced colleagues appear together in these descriptions.</p>



<h1 class="wp-block-heading" id="l5-senior-engineering-and-system-ownership">L5 Senior engineering and system ownership</h1>



<p class="wp-block-paragraph">Globally popular streaming platform has advertised Senior Software Engineer positions at L5. A useful example is its Analysis role in the Experimentation Platform team, which combines responsibility for production systems with input into architecture and the team&#8217;s roadmap.</p>



<p class="wp-block-paragraph">The archived Analysis listing describes workflows processing billions of events each day. Its responsibilities include developing experiment-analysis tooling, responding to incidents, improving reliability, and agreeing data and API contracts with other teams. <strong>The engineer is also expected to understand how product teams use experiments</strong>. These details show that the job extends beyond implementing a predefined feature.</p>



<p class="wp-block-paragraph">In this role, senior responsibility includes <strong>accounting for the operational consequences of a design</strong> and the needs of the teams using it. Netflix&#8217;s emphasis on independent judgment provides context for that work, but the Culture Memo applies across the company rather than defining separate expectations for L5.</p>



<h1 class="wp-block-heading" id="l6-staff-engineering-across-teams">L6 Staff engineering across teams</h1>



<p class="wp-block-paragraph">At L6, Netflix explicitly advertises <strong>Staff Software Engineer roles</strong>. The AIMS Platform Systems examples make the wider scope concrete: the engineer works on infrastructure used by other engineering and research teams.</p>



<p class="wp-block-paragraph">The <strong>Developer Productivity role</strong> covers development environments, build and test systems, CI/CD, and tools used to move research into production experiments. It also calls for measuring build times and iteration speed, supporting adoption, and setting shared technical standards.</p>



<p class="wp-block-paragraph">The <strong>Staff ML role</strong> focuses on observability, infrastructure cost, and reliability, alongside contributing to architecture and migration planning with other teams.</p>



<p class="wp-block-paragraph">These examples suggest <strong>that Staff responsibility includes getting shared systems adopted and making them useful across AIMS</strong>. The developer productivity posting makes that test concrete: the tools need to improve how other teams build and run experiments. The work combines hands-on engineering with coordination across teams.</p>



<h1 class="wp-block-heading" id="l7-technical-direction-across-an-organization">L7 Technical direction across an organization</h1>



<p class="wp-block-paragraph">Levels.fyi calls Netflix L7 Principal Engineer, while the Growth Engineering job description uses Software Engineer L7. Its responsibilities are defined around the Growth Engineering organization, so the scope is more informative than the title alone.</p>



<p class="wp-block-paragraph">The published role reports to a Senior Director <strong>and asks for at least 15 years of professional software development experience as an individual contributor</strong>. It includes setting architectural direction across Growth Engineering, connecting business goals to technology investments, and working with senior leaders and other L7 engineers. The experience requirement belongs to this vacancy, not to a published rule for every L7 role.</p>



<p class="wp-block-paragraph">The remit is broad technical leadership within Growth Engineering. It combines architectural decisions with continued expertise in production systems and mentoring other engineers, including helping managers support their teams&#8217; development.</p>



<h1 class="wp-block-heading" id="comparing-level-names-across-companies">Comparing level names across companies</h1>



<p class="wp-block-paragraph">Level numbers are company-specific. Levels.fyi places <a href="https://shiftmag.dev/google-career-ladder-explained-for-engineers-10368/" data-type="post" data-id="10368" target="_blank" rel="noreferrer noopener">Google&#8217;s Senior Software Engineer title</a> at L5 and Staff at L6, while Amazon uses L6 for Senior SDE and L7 for Principal SDE. Meta uses E-level labels in the same database. These naming conventions can help a candidate navigate vacancies, but they do not establish equivalent responsibilities or hiring standards.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="261" src="https://shiftmag.dev/wp-content/uploads/2026/10/Screenshot-2026-10-09-at-12.51.02-1024x261.png?x32039" alt="" class="wp-image-12585" srcset="https://shiftmag.dev/wp-content/uploads/2026/10/Screenshot-2026-10-09-at-12.51.02-1024x261.png 1024w, https://shiftmag.dev/wp-content/uploads/2026/10/Screenshot-2026-10-09-at-12.51.02-766x195.png 766w, https://shiftmag.dev/wp-content/uploads/2026/10/Screenshot-2026-10-09-at-12.51.02-300x76.png 300w, https://shiftmag.dev/wp-content/uploads/2026/10/Screenshot-2026-10-09-at-12.51.02.png 1147w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">The distinction matters most above Senior. For example, Google&#8217;s L7 is Senior Staff and its Principal title appears at L8, whereas Levels.fyi labels Netflix L7 Principal. <strong>A shared title or similar number does not show where an engineer would be placed after changing companies</strong>. The role&#8217;s technical remit and the employer&#8217;s assessment remain necessary context.</p>



<h1 class="wp-block-heading" id="how-culture-affects-engineering-work">How culture affects engineering work</h1>



<p class="wp-block-paragraph">Netflix&#8217;s <a href="https://jobs.netflix.com/culture" target="_blank" rel="noreferrer noopener">Culture Memo</a> highlights <strong>judgment, candid feedback, and giving employees enough context to make decisions</strong>. It also expects managers to stay involved and coach their teams. Autonomy should therefore not be mistaken for an absence of support, nor treated as a benefit reserved for senior levels.</p>



<p class="wp-block-paragraph">The keeper test is part of that performance culture. Managers are asked whether they would try to retain someone or choose to hire them again.<strong> If the answer is no, Netflix says it favors ending the employment relationship promptly</strong>. The memo also says to assess a person&#8217;s overall record, discuss performance regularly, and support employees through short-term setbacks.</p>



<p class="wp-block-paragraph">For a candidate, the practical next step is to ask how the hiring team defines scope at the advertised level, what support is available, and how progression is assessed. Public postings can help frame that conversation. They cannot supply a guaranteed promotion timetable or the terms of an individual offer.</p>


<figure class="wp-block-post-featured-image"><img loading="lazy" decoding="async" width="1672" height="941" src="https://shiftmag.dev/wp-content/uploads/2026/10/netflix-career-ladder.png?x32039" class="attachment-post-thumbnail size-post-thumbnail wp-post-image" alt="" style="object-fit:cover;" srcset="https://shiftmag.dev/wp-content/uploads/2026/10/netflix-career-ladder.png 1672w, https://shiftmag.dev/wp-content/uploads/2026/10/netflix-career-ladder-300x169.png 300w, https://shiftmag.dev/wp-content/uploads/2026/10/netflix-career-ladder-768x432.png 768w, https://shiftmag.dev/wp-content/uploads/2026/10/netflix-career-ladder-1024x576.png 1024w" sizes="auto, (max-width: 1672px) 100vw, 1672px" /></figure><p>The post <a href="https://shiftmag.dev/netflix-engineering-levels-explained-l3-to-l7-career-ladder-scope-and-pay-12572/">Netflix Engineering Levels Explained: L3 to L7 Career Ladder, Scope, and Pay</a> appeared first on <a href="https://shiftmag.dev">ShiftMag</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>How 5,000 RPS Exposed the Real Bottleneck in Our Jetty Messaging Service</title>
		<link>https://shiftmag.dev/performance-tuning-is-about-finding-the-real-bottleneck-11823/</link>
		
		<dc:creator><![CDATA[Slawomir Luty]]></dc:creator>
		<pubDate>Fri, 09 Oct 2026 12:50:10 +0000</pubDate>
				<category><![CDATA[Backend]]></category>
		<category><![CDATA[Software Engineering]]></category>
		<category><![CDATA[performance testing]]></category>
		<category><![CDATA[reactive programming]]></category>
		<category><![CDATA[virtual threads]]></category>
		<guid isPermaLink="false">https://shiftmag.dev/?p=11823</guid>

					<description><![CDATA[<p>We started with a simple question: how much traffic can the service really handle? The answer was in understanding the system’s real limits.</p>
<p>The post <a href="https://shiftmag.dev/performance-tuning-is-about-finding-the-real-bottleneck-11823/">How 5,000 RPS Exposed the Real Bottleneck in Our Jetty Messaging Service</a> appeared first on <a href="https://shiftmag.dev">ShiftMag</a>.</p>
]]></description>
										<content:encoded><![CDATA[<figure class="wp-block-post-featured-image"><img loading="lazy" decoding="async" width="1350" height="709" src="https://shiftmag.dev/wp-content/uploads/2026/09/measure-your-service.png?x32039" class="attachment-post-thumbnail size-post-thumbnail wp-post-image" alt="" style="object-fit:cover;" srcset="https://shiftmag.dev/wp-content/uploads/2026/09/measure-your-service.png 1350w, https://shiftmag.dev/wp-content/uploads/2026/09/measure-your-service-300x158.png 300w, https://shiftmag.dev/wp-content/uploads/2026/09/measure-your-service-1024x538.png 1024w, https://shiftmag.dev/wp-content/uploads/2026/09/measure-your-service-768x403.png 768w" sizes="auto, (max-width: 1350px) 100vw, 1350px" /></figure>


<p class="wp-block-paragraph">In my team in Infobip, we handle message sending and serve as an endpoint for OTT providers such as WhatsApp, Viber, and Apple. Because of that, <strong>message sending is our core responsibility</strong>, and we need to be aware of any bottlenecks.</p>



<p class="wp-block-paragraph">Initially, our service was measured at around 1,000 requests per second, but we didn’t know why. </p>



<p class="wp-block-paragraph">From time to time, this raised concerns in the team: <strong>we should understand better where the bottleneck is</strong> and what causes it. Once we know that, we can make informed decisions to address client needs &#8211; for example, answering whether we can handle the traffic they expect to send. That was partly what led to this investigation.</p>



<h2 class="wp-block-heading" id="how-to-run-the-tests">How to run the tests</h2>



<h3 class="wp-block-heading" id="application">Application</h3>



<p class="wp-block-paragraph">Initially, with no experience in performance testing, I viewed the service as a set of methods &#8211; essentially a stack of calls. I assumed I could analyze it with a profiler. I also knew I shouldn’t run it on my computer, because other applications would affect the service’s performance.</p>



<p class="wp-block-paragraph">That turned out to be a bad assumption. I’d suggest starting by <strong>measuring the application as a whole</strong> to get a broader view of its performance.</p>



<h3 class="wp-block-heading" id="integration">Integration</h3>



<p class="wp-block-paragraph">Since our application forwards messages further down the infrastructure and ultimately to the provider, it had to be adapted so that it would not send real traffic, while preserving as much of the original logic as possible.</p>



<p class="wp-block-paragraph">Initial tests of this version showed that the service could handle a request in 0 ms, which seemed unrealistic. I realized that I needed to introduce a <strong>delay based on production</strong>, where the rounded average request processing time is around 50 ms.</p>



<h3 class="wp-block-heading" id="initial-measurements">Initial measurements</h3>



<p class="wp-block-paragraph">Having established that the service handles one request in 50 ms, we can estimate that it can process about 20 requests per second. Since the service is Jetty-based, it has roughly 200 threads available by default, which gives a theoretical peak of around<strong> 4,000 requests per second</strong>. With that in place, the service can now be tested.</p>



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



<ul class="wp-block-list">
<li>DO NOT run such tests on a local machine.</li>



<li>Dedicated machine on our testing env was created for it.</li>



<li>K6 was chosen as test suite.</li>



<li>Rundeck to automate and parameterize remotely run tests.</li>
</ul>



<h3 class="wp-block-heading" id="endpoints-to-be-tested-against">Endpoints to be tested against:</h3>



<ul class="wp-block-list">
<li><code>/mapi-mock/1/dummy</code>&nbsp;&#8211; performs <code>Thread.sleep(50)</code> &#8211; so it is blocking code run on Jetty thread pool</li>



<li><code>/mapi-mock/1/dummyReactor</code>&nbsp;&#8211; also performs&nbsp;<code>Thread.sleep(50)</code>but publishing result on boundedElastic thread pool &#8211; it is still blocking code, but in that case one thread of reactor&#8217;s pool will be used</li>



<li><code>/mapi-mock/1/messages</code>&nbsp;&#8211; non-blocking code which sends request through WebClient to real service</li>



<li><code>/mapi-mock/1/messagesReactor</code>&nbsp;&#8211; non-blocking code which sends request through WebClient but publishing it on boundedElastic</li>
</ul>



<h3 class="wp-block-heading" id="jvm-tuning">JVM tuning</h3>



<p class="wp-block-paragraph">Make sure the correct memory parameter is used. In my case, <code>HEAP_PERCENT</code> was set to 60, meaning the JVM heap used 60% of the machine’s RAM. That allowed me to focus on machine RAM only, and the heap size would be adjusted automatically. This is not ideal, but it was good enough for our tests.</p>



<h3 class="wp-block-heading" id="metrics-gathering">Metrics gathering</h3>



<p class="wp-block-paragraph">Initially, I ran the tests for 40 seconds, but I forgot that the metrics were collected every 30 seconds. That explains why I didn’t see any issues in the service metrics during the first run. When I extended the test duration to 180 seconds (a value I borrowed from similar tests) everything became visible.</p>



<p class="wp-block-paragraph"><strong>You can also observe the machine directly with tools like <code>htop</code> or <code>top</code></strong>, or any other tool that shows current CPU and memory usage. In the end, you can use whichever tool helps you complete the task.</p>



<p class="wp-block-paragraph">One obvious but important rule: <strong>do not change two variables at the same time unless you know what you are doing</strong>. If you do, you won’t know which parameter had the effect. Change one thing, test it, then change the next thing, and repeat until you understand what is happening.know what is going on.</p>



<h2 class="wp-block-heading" id="gathering-measurements">Gathering measurements</h2>



<p class="wp-block-paragraph">Initially, <strong>I decided to send up to 5k requests toward the service</strong> &#8211; it is below theoretical machine efficiency, but higher than what was experienced in the past. Lower CPU and RAM measurements are somewhat weird: 4 GB of RAM is not recommended as a base machine setup, because there is less than 2 GB of RAM left for the operating system, where not only our service lives, but also supporting applications. But both measurements for 2 CPU showed that it is way too low for handling any traffic efficiently and it showed quite high CPU and RAM usage, meaning that it could be improved. I finished the tests with 5k requests/s, when performance was quite near predicted performance.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="643" src="https://shiftmag.dev/wp-content/uploads/2026/10/s2-1024x643.jpg?x32039" alt="" class="wp-image-12547" srcset="https://shiftmag.dev/wp-content/uploads/2026/10/s2-1024x643.jpg 1024w, https://shiftmag.dev/wp-content/uploads/2026/10/s2-768x482.jpg 768w, https://shiftmag.dev/wp-content/uploads/2026/10/s2-300x188.jpg 300w, https://shiftmag.dev/wp-content/uploads/2026/10/s2.webp 1088w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">The theoretical peak was reached, so it was time to check whether it could be improved. <strong>I tried switching from Jetty pool to Reactor pool</strong>, but it turned out (my bad for not remembering Project Reactor documentation by heart) that to reach the initial performance I had to configure the machine with 20 CPU, because Reactor constructs the pool to have 10 * CPU threads. So when I matched theoretical performance again, but now with the Reactor pool, I thought about what could be done next to improve. The last thing to check was how non-blocking code would perform, so I started using the <code>/mapi-mock/1/messages*</code> endpoints for testing. As one would predict, it was hit. The application peaked at 7,000 requests/s, but with quite large demands for hardware. So the last thing to check was to try to find reasonable settings to have big enough performance with not much resources assigned.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="574" src="https://shiftmag.dev/wp-content/uploads/2026/10/s3-1024x574.jpg?x32039" alt="" class="wp-image-12548" srcset="https://shiftmag.dev/wp-content/uploads/2026/10/s3-1024x574.jpg 1024w, https://shiftmag.dev/wp-content/uploads/2026/10/s3-300x168.jpg 300w, https://shiftmag.dev/wp-content/uploads/2026/10/s3-767x430.jpg 767w, https://shiftmag.dev/wp-content/uploads/2026/10/s3.webp 1339w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">Now we are focusing on the <strong>non-blocking endpoint</strong>, so it more reflects the real-case scenario, and on limiting resources. The first thing that is apparent is that when it comes to non-blocking code, there is no difference between Jetty thread pool and boundedElastic thread pool (the latter results in some queuing because the rule of 10 * CPU threads is always true). We can see performance downgrade with CPUs removal, but not apparently linear. I had to say &#8220;stop&#8221; somewhere, and I decided to stop with 8 CPU and 8 GB of RAM, for which we received ~3.5k requests/s. Good enough with not much resources assigned. As we can scale horizontally, adding more machines will use more resources, but give more resiliency back.<img decoding="async" srcset="https://media-cdn.atlassian.com/file/8dff5700-9afa-4aae-a04c-bd06a3193a66/image/cdn?allowAnimated=true&amp;client=978ef90e-12de-4e06-8440-5a847a8913b7&amp;collection=contentId-575373490&amp;height=125&amp;max-age=2592000&amp;mode=full-fit&amp;source=mediaCard&amp;token=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiI5NzhlZjkwZS0xMmRlLTRlMDYtODQ0MC01YTg0N2E4OTEzYjciLCJhY2Nlc3MiOnsidXJuOmZpbGVzdG9yZTpjb2xsZWN0aW9uOmNvbnRlbnRJZC01NzUzNzM0OTAiOlsicmVhZCJdfSwiZXhwIjoxNzg4MzU0NzY5LCJuYmYiOjE3ODgzNTE4ODksImFhSWQiOiI3MTIwMjA6OGQ5MzU5MzEtNjhjZC00MDkyLWE4MTQtOThiYjM3Y2NhYTFhIiwiaHR0cHM6Ly9pZC5hdGxhc3NpYW4uY29tL2FwcEFjY3JlZGl0ZWQiOmZhbHNlLCJhdXRoVHlwZSI6InNlc3Npb24ifQ.FyDYJ8GOtTYn3qys-d5COcquLC4e2LvFovDCLTvyxwU&amp;width=1339#media-blob-url=true&amp;id=8dff5700-9afa-4aae-a04c-bd06a3193a66&amp;clientId=978ef90e-12de-4e06-8440-5a847a8913b7&amp;contextId=contentId-575373490&amp;collection=contentId-575373490 1x, https://media-cdn.atlassian.com/file/8dff5700-9afa-4aae-a04c-bd06a3193a66/image/cdn?allowAnimated=true&amp;client=978ef90e-12de-4e06-8440-5a847a8913b7&amp;collection=contentId-575373490&amp;height=250&amp;max-age=2592000&amp;mode=full-fit&amp;source=mediaCard&amp;token=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiI5NzhlZjkwZS0xMmRlLTRlMDYtODQ0MC01YTg0N2E4OTEzYjciLCJhY2Nlc3MiOnsidXJuOmZpbGVzdG9yZTpjb2xsZWN0aW9uOmNvbnRlbnRJZC01NzUzNzM0OTAiOlsicmVhZCJdfSwiZXhwIjoxNzg4MzU0NzY5LCJuYmYiOjE3ODgzNTE4ODksImFhSWQiOiI3MTIwMjA6OGQ5MzU5MzEtNjhjZC00MDkyLWE4MTQtOThiYjM3Y2NhYTFhIiwiaHR0cHM6Ly9pZC5hdGxhc3NpYW4uY29tL2FwcEFjY3JlZGl0ZWQiOmZhbHNlLCJhdXRoVHlwZSI6InNlc3Npb24ifQ.FyDYJ8GOtTYn3qys-d5COcquLC4e2LvFovDCLTvyxwU&amp;width=2678#media-blob-url=true&amp;id=8dff5700-9afa-4aae-a04c-bd06a3193a66&amp;clientId=978ef90e-12de-4e06-8440-5a847a8913b7&amp;contextId=contentId-575373490&amp;collection=contentId-575373490 2x" src="https://media-cdn.atlassian.com/file/8dff5700-9afa-4aae-a04c-bd06a3193a66/image/cdn?allowAnimated=true&amp;client=978ef90e-12de-4e06-8440-5a847a8913b7&amp;collection=contentId-575373490&amp;height=125&amp;max-age=2592000&amp;mode=full-fit&amp;source=mediaCard&amp;token=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiI5NzhlZjkwZS0xMmRlLTRlMDYtODQ0MC01YTg0N2E4OTEzYjciLCJhY2Nlc3MiOnsidXJuOmZpbGVzdG9yZTpjb2xsZWN0aW9uOmNvbnRlbnRJZC01NzUzNzM0OTAiOlsicmVhZCJdfSwiZXhwIjoxNzg4MzU0NzY5LCJuYmYiOjE3ODgzNTE4ODksImFhSWQiOiI3MTIwMjA6OGQ5MzU5MzEtNjhjZC00MDkyLWE4MTQtOThiYjM3Y2NhYTFhIiwiaHR0cHM6Ly9pZC5hdGxhc3NpYW4uY29tL2FwcEFjY3JlZGl0ZWQiOmZhbHNlLCJhdXRoVHlwZSI6InNlc3Npb24ifQ.FyDYJ8GOtTYn3qys-d5COcquLC4e2LvFovDCLTvyxwU&amp;width=1339#media-blob-url=true&amp;id=8dff5700-9afa-4aae-a04c-bd06a3193a66&amp;clientId=978ef90e-12de-4e06-8440-5a847a8913b7&amp;contextId=contentId-575373490&amp;collection=contentId-575373490" alt=""></p>



<h2 class="wp-block-heading" id="the-thing-with-vt">&nbsp;The thing with VT&#8230;</h2>



<p class="wp-block-paragraph">As you can see, I focused on measuring reactive vs non-reactive code. </p>



<p class="wp-block-paragraph">It was our go-to architecture at that time, while VT was being adopted slowly (we were afraid of thread pinning, so we waited until it was addressed in JDK 24 to start adoption). After I published it internally, I was asked specifically about VT &#8211; how the app would perform when configured to use this not-so-new-but-fixed feature. </p>



<p class="wp-block-paragraph"><strong>It turned out that VT gave the app a boost.</strong> With optimal configuration (8 CPU and 8 GB of RAM) the app was able to handle 10k requests/s. Why is that? Any code, regardless of whether it is blocking or not, will be run on a virtual thread, which ideally should not block a system thread, leading to the case where system threads are used only for real logic and not for waiting. VT can improve applications where there is a lot of blocking code. In the rest of the cases, it is &#8220;just&#8221; syntactic freedom, no more looking at <a href="https://projectreactor.io/docs/core/release/reference/apdx-operatorChoice.html" target="_blank" rel="noreferrer noopener">which operator do I need</a>?</p>



<h2 class="wp-block-heading" id="the-outcome">The outcome</h2>



<p class="wp-block-paragraph">When I performed the tests, I observed that just switching to <code>boundedElastic</code> (i.e. “use Reactor and you will get higher TPS”) didn’t increase throughput &#8211; it actually reduced it. Not remembering the Project Reactor documentation by heart, I was surprised, but after some research I found that <code>boundedElastic</code> provides <code>10 * CPU</code> threads, which gave us a total of 60 threads to service all requests previously handled by 200 Jetty threads.</p>



<p class="wp-block-paragraph">When the CPU count was adjusted to match 200 Jetty threads (20 CPUs) we reached the expected throughput. But it was wasteful in terms of resources. In the Reactor world, people often say that blocking is bad, and my service was actually doing exactly that, on purpose. I thought it was time to switch to a different endpoint.</p>



<p class="wp-block-paragraph">So <strong>I was able to nearly reach the estimated performance of the service</strong>, which was good. That allowed me to move on and investigate other ways of improving it, such as using non-blocking code or VT.</p>



<h2 class="wp-block-heading" id="what-i%e2%80%99ve-learned">What I’ve learned</h2>



<p class="wp-block-paragraph">It is always about<strong> finding the bottleneck and finding out if we can improve it</strong>, for example by adding resources or redesigning it. If it is not to be improved, then this is the peak capacity of the service.</p>



<p class="wp-block-paragraph"><strong>Sometimes questions from clients can actually help</strong>. It is just like with a new person joining the team: they will ask obvious questions about all-known solutions, but if you want to respond accurately, you have to dig deeper, and sometimes you find out that this simple question leads to an investigation and even to questioning whether it is correct at all.</p>



<p class="wp-block-paragraph">Also,<strong> be aware of architecture</strong>. In that scenario, we tested a service which accepts requests, so it is one of the first applications, or APIs, which the client experiences. Being first means that there is the rest of the system, which can in turn have throttling applied. What does it mean? Even if we accept 10k requests/s, sometimes we cannot send them immediately, because an independent system cannot handle, for example, more than 2k requests/s. So we can be fast for clients, in the sense that we accept their traffic as fast as they want, but we should always remember that this is not the whole picture, and the client should be informed about it.</p>



<p class="wp-block-paragraph"><strong>In my test there was no DB, just API-to-API calls</strong>. Having any limited, and even worse blocking, resource like a DB in the way of processing a request will definitely impact its performance.</p>
<p>The post <a href="https://shiftmag.dev/performance-tuning-is-about-finding-the-real-bottleneck-11823/">How 5,000 RPS Exposed the Real Bottleneck in Our Jetty Messaging Service</a> appeared first on <a href="https://shiftmag.dev">ShiftMag</a>.</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Page Caching using Disk: Enhanced 

Served from: shiftmag.dev @ 2026-11-01 16:33:25 by W3 Total Cache
-->