<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Dor Amram on Medium]]></title>
        <description><![CDATA[Stories by Dor Amram on Medium]]></description>
        <link>https://medium.com/@doramram210?source=rss-b56441e272bc------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*FY3eDgEe5h98Wr9-LbLqHA.png</url>
            <title>Stories by Dor Amram on Medium</title>
            <link>https://medium.com/@doramram210?source=rss-b56441e272bc------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 06 Jun 2026 21:11:14 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@doramram210/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Judgment Day: The Role of the Software Engineer in the Age of AI]]></title>
            <link>https://medium.com/@doramram210/judgment-day-the-role-of-the-software-engineer-in-the-age-of-ai-27e59ba1c606?source=rss-b56441e272bc------2</link>
            <guid isPermaLink="false">https://medium.com/p/27e59ba1c606</guid>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[software-architecture]]></category>
            <category><![CDATA[software-development]]></category>
            <dc:creator><![CDATA[Dor Amram]]></dc:creator>
            <pubDate>Sun, 24 May 2026 08:21:59 GMT</pubDate>
            <atom:updated>2026-05-24T08:21:59.017Z</atom:updated>
            <content:encoded><![CDATA[<h3>AI can write your code. It cannot make your decisions. The engineer who survives is the one whose judgment AI makes more dangerous, not more replaceable.</h3><p>There is a particular kind of panic running through the software industry right now. It lives in Slack threads and conference hallways. It sounds like this: <em>If AI can write code, what exactly am I for?</em></p><p>It is the wrong question. Not because AI cannot write code — it can, increasingly well — but because it confuses the activity with the value. A software engineer’s value was never in typing syntax. It was in deciding <em>what</em> to build, <em>how</em> the pieces fit, and <em>what to leave out</em>. These are acts of <strong>judgment</strong>. And judgment is the one thing AI cannot automate, because judgment requires accountability — someone who lives with the consequences.</p><p>The engineers who will thrive in the age of AI are not the ones who resist it or the ones who surrender to it. They are the ones whose architectural judgment turns AI from a parlor trick into a force multiplier.</p><p>This essay is about what that judgment looks like in practice.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pooCZkwSNlc8KdiLlWaQLQ.png" /></figure><h3>1. The Judgment Gap</h3><p>AI can generate a function that parses JSON. It can scaffold a REST API. It can write a passable database migration. What it cannot do is answer the question that precedes all of these: <strong>should this exist at all?</strong></p><p>That question is architectural. It lives upstream of code. It asks: does this service need to be separate, or is it premature decomposition that will cost us three years of distributed debugging? Should this data be denormalized for read performance, or will that decision make the inevitable schema migration a nightmare? Is this the right abstraction boundary, or are we drawing lines based on our org chart rather than our domain model?</p><p>These decisions are not hard because we lack information. They are hard because they require weighing incommensurable concerns — development speed against operational complexity, team autonomy against system coherence, present convenience against future optionality. They are irreducible judgment calls.</p><p><em>The staff engineer is not the one who writes code quickly. It is the one who decides correctly which code should never be written.</em></p><p>AI collapses the cost of producing code toward zero. That sounds like it makes engineers less valuable. In fact, it makes <em>judgment</em> more valuable, because the cost of producing the wrong code has also collapsed toward zero — which means organizations will produce far more wrong code, far faster, unless someone is applying judgment at the architectural layer.</p><h3>2. Core Architectural Decisions That AI Cannot Make</h3><p>Let’s be concrete. Here are the categories of decisions where human judgment remains irreplaceable — not because AI lacks knowledge, but because these decisions require owning trade-offs that have no objectively correct answer.</p><h3>System Boundaries</h3><p>Where do you draw the lines between services, modules, or teams? This is simultaneously a technical decision (coupling, cohesion, data ownership) and an organizational one (Conway’s Law is real). AI can suggest microservice boundaries based on domain analysis. It cannot know that your payments team just lost two senior engineers and cannot own an independent service right now. That context is judgment.</p><h3>Consistency vs. Availability</h3><p>The CAP theorem is not a trivia question — it is a business decision. When the network partitions, does your system prioritize returning a correct answer (at the cost of availability) or returning some answer (at the cost of correctness)? An e-commerce cart can tolerate stale data. A financial ledger cannot. AI can explain the theorem. It cannot decide which trade-off your business can survive.</p><h3>Build vs. Adopt vs. Defer</h3><p>Every component in your system represents a choice: build it yourself, adopt an existing tool, or defer the decision entirely. AI biases toward building because generating code is what it does. An experienced architect knows that the best architectural decision is often <em>not yet</em> — keeping options open until the problem is better understood. This is negative capability, and it is one of the hardest skills to develop.</p><h3>Failure Mode Design</h3><p>Every system fails. The architectural question is not “how do we prevent failure” but “how does this system degrade?” Do orders queue and retry, or do they fail immediately with a clear error? Does the recommendation engine fall back to popularity-based suggestions, or does it show nothing? These are product decisions expressed as architecture, and they require understanding the business impact of each failure scenario.</p><h3>Reversibility Assessment</h3><p>Some decisions are two-way doors: easy to reverse if wrong. Others are one-way doors: the migration to a new database engine, the public API contract, the choice of cloud provider at scale. The judgment lies in classifying decisions correctly. Over-deliberating a two-way door wastes time. Under-deliberating a one-way door creates years of regret.</p><h3>3. AI as Judgment Amplifier</h3><p>Here is the shift that most commentary on AI and engineering misses: AI does not replace judgment. It <strong>amplifies</strong> it. An engineer with good judgment and access to AI is dramatically more effective than either alone. An engineer with bad judgment and access to AI is dramatically more dangerous.</p><p>Think of it as leverage. A lever does not create force — it multiplies the force you apply. If you push in the wrong direction, a longer lever just gets you further from where you want to be, faster.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lXeVy3rVKBLvb2QiXK0bMA.png" /></figure><h3>What amplification looks like in practice</h3><p><strong>Good judgment + AI:</strong></p><ul><li>Architect decides on event-driven decoupling → AI generates the consumer/producer boilerplate in minutes</li><li>Engineer identifies the right abstraction boundary → AI scaffolds the interface and tests</li><li>Team chooses eventual consistency → AI implements the saga pattern with proper compensation logic</li><li>Architect defines failure modes → AI builds circuit breakers, retries, and fallbacks to spec</li></ul><p><strong>Poor judgment + AI:</strong></p><ul><li>Premature microservices split → AI generates five services that should have been one module</li><li>Wrong abstraction boundary → AI produces a beautifully tested interface nobody should call</li><li>Unnecessary complexity adopted “because AI made it easy” → team drowns in operational burden</li><li>No failure mode analysis → AI builds a happy-path system that shatters on first contact with reality</li></ul><p>The pattern is consistent: AI accelerates execution, but the direction of execution is set by architectural judgment. Get the direction right, and AI gives you velocity that was previously impossible. Get it wrong, and AI helps you build the wrong thing at unprecedented speed.</p><h3>4. The Judgment Stack</h3><p>If judgment is a durable skill, what does it consist of? Not abstract wisdom — judgment is a stack of concrete capabilities that can be developed, practiced, and refined.</p><p><strong>The Judgment Stack — what AI can’t substitute</strong></p><p>Layer 4: BUSINESS CONTEXT</p><p>What does the business need? What constraints are non-negotiable? What can we sacrifice?</p><p>→ Requires organizational knowledge AI lacks</p><p>Layer 3: TRADE-OFF EVALUATION</p><p>Given N viable options, which set of costs is this organization best equipped to bear?</p><p>→ Requires value judgments, no “correct” answer</p><p>Layer 2: SYSTEM-LEVEL REASONING</p><p>How do components interact under stress? Where are the hidden couplings?</p><p>→ AI helps explore; humans must validate</p><p>Layer 1: PATTERN RECOGNITION</p><p>Which architectural patterns apply here? What worked in similar contexts?</p><p>→ AI strong here, but pattern ≠ decision</p><p>Layer 0: IMPLEMENTATION</p><p>Write the code, configure the infra</p><p>→ AI handles most of this now</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LHHhtrCIc-ypx6FNFedStg.png" /></figure><p>AI is strong at Layers 0 and 1. It can write code and recognize patterns. It is useful but unreliable at Layer 2 — it can model system interactions but misses emergent behaviors and implicit coupling. It is weak at Layers 3 and 4, because these require reasoning about values, organizational dynamics, and consequences that extend beyond the technical system.</p><p>The engineer who invests in Layers 3 and 4 becomes more valuable as AI improves at Layers 0 and 1. The engineer who stays at Layer 0 is competing directly with a tool that works 24 hours a day and never asks for a raise.</p><h3>5. How to Develop Architectural Judgment</h3><p>Judgment is not innate. It is scar tissue from decisions made, consequences observed, and lessons internalized. Here is how to accelerate its development.</p><h3>Study failures, not successes</h3><p>Success is ambiguous — it could be good design or good luck. Failure is informative. Read post-mortems. Study systems that collapsed under scale, migrations that went sideways, abstractions that leaked. Every failure is a lesson in judgment that someone else paid for.</p><h3>Make decisions, then live with them</h3><p>You cannot develop judgment by observing decisions. You develop it by making them, owning the consequences, and adjusting. This means seeking responsibility for architectural choices, not avoiding it. Volunteer for the database migration. Own the API design. Choose the messaging system. When it goes wrong — and it will — you learn something that no book or AI can teach you.</p><h3>Write Architecture Decision Records</h3><p>Document your decisions, the alternatives you considered, and why you chose what you chose. Six months later, revisit them. Which held up? Which did you get wrong? The act of writing forces clarity; the act of revisiting builds calibration.</p><h3>Argue the other side</h3><p>Whatever architectural position you hold, practice arguing the opposite. If you chose Kafka, argue for RabbitMQ. If you went serverless, make the case for containers. If you cannot make a compelling case for the alternative, you do not actually understand your own decision — you just have a preference.</p><h3>Use AI to stress-test your thinking</h3><p>Here is where AI becomes a genuine tool for developing judgment: use it as a sparring partner. Present your architecture to an AI and ask it to find the weaknesses. Ask it to generate failure scenarios. Ask it what a critic would say. This is not AI replacing your judgment — it is AI sharpening it.</p><h3>6. The New Engineering Career Trajectory</h3><p>The traditional engineering career ladder — junior writes code, senior writes better code, staff writes the hardest code — is collapsing. AI compresses the lower rungs. A mid-level engineer with AI can ship features that used to require a senior. This does not eliminate levels. It redefines what each level means.</p><p>The new career trajectory is not writing harder code. It is making harder decisions with higher stakes and broader consequences.</p><p>The <strong>junior</strong> engineer’s job is no longer “learn to write code.” It is “learn to evaluate AI-generated code, understand why it works, and recognize when it doesn’t.”</p><p>The <strong>senior</strong> engineer’s job is no longer “write complex systems.” It is “design systems that are correct, make trade-offs explicit, and guide teams through ambiguity.”</p><p>The <strong>staff</strong> engineer’s job is no longer “solve the hardest technical problems.” It is “decide which problems are worth solving, align technical strategy with business strategy, and create the conditions for good judgment to flourish across the organization.”</p><p>At every level, the differentiator is judgment. The tools change. The judgment endures.</p><h3>7. The Uncomfortable Corollary</h3><p>If judgment is a durable skill, then engineers who never develop it are genuinely at risk. If an organization can get the same code output from fewer people, and the remaining people are differentiated by their judgment, then the engineers who only execute without deciding will find their roles compressed.</p><p>This is not a threat to wave around for dramatic effect. It is a structural change that is already happening. The response is not to panic. It is to invest in the right skills — to move up the judgment stack deliberately, to seek architectural responsibility proactively, and to treat AI as a lever that makes your judgment reach further rather than a replacement that makes your coding obsolete.</p><p>The engineers who do this will have the ability to execute at a speed and scale that was previously reserved for entire teams, directed by judgment that no model can replicate. That combination — human judgment amplified by machine capability — is not the end of the software engineer. It is the beginning of the software engineer who actually matters.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*sVr8qmNtRnPEp__TZ5thRA.png" /></figure><p><strong>Judgment day is not the day machines replace you.</strong> It is every day you show up and make a decision that a machine cannot. The question is not whether that day is coming. It is whether you are building the judgment to survive it.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=27e59ba1c606" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Endless Puzzle: AI Agents in Production Systems]]></title>
            <link>https://medium.com/@doramram210/the-endless-puzzle-ai-agents-in-production-systems-5e4e8c1dc882?source=rss-b56441e272bc------2</link>
            <guid isPermaLink="false">https://medium.com/p/5e4e8c1dc882</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[architecture]]></category>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[software-development]]></category>
            <dc:creator><![CDATA[Dor Amram]]></dc:creator>
            <pubDate>Thu, 16 Oct 2025 11:41:03 GMT</pubDate>
            <atom:updated>2025-10-16T11:41:03.479Z</atom:updated>
            <content:encoded><![CDATA[<p>Software engineering has always been about solving a two-part puzzle. On one side, there’s the real world: messy, unpredictable, constantly shifting. On the other, there’s your code: deterministic, logical, following exactly the rules you gave it. When these two pieces align, your system does exactly what it needs to do. Most of the time, though, there’s a gap.</p><p>That gap is where things get interesting.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fazW6_PV2e3BBlbUFwF8NQ.png" /></figure><h3>The Problem with Perfect Logic</h3><p>Traditional code requires you to anticipate scenarios. You write decision trees, add conditions, build in edge cases. It works until the world throws something you didn’t predict in your planning session six months ago. I’ve been on teams that tried to solve this by adding more rules (more if-then statements, more configuration files, more attempts to encode every possible situation). You end up with something that handles known cases perfectly and fails in novel ones.</p><p>The fundamental tension exists between rigid instruction sets and environments that refuse to be predictable. This has always been true.</p><h3>What Agents Actually Change</h3><p>Over the past year, I’ve worked with several teams implementing agents in production. I’ve also been part of building the main agent system at Similarweb (one that takes user requests, analyzes our data, and generates whatever assets users need). I spent the first three months fairly convinced we were overcomplicating things. Why add this layer of apparent unpredictability to systems that need to be reliable?</p><p>The shift came when I stopped asking “do we need agents” and started asking “which parts of this system have the right structure for agent-based approaches.”</p><p>Consider order execution timing. You can write logic for when to execute based on price thresholds and volume. But what about breaking news that hasn’t fully propagated through the market? What about unusual options activity suggesting informed traders know something? Traditional code needs you to have predicted these exact scenarios and coded for them. An agent can observe patterns, connect information that wasn’t explicitly programmed, and adjust. It can reason about context in ways that brute-force rules can’t.</p><p>Agents handle the subset of problems where the solution space is too large to enumerate but the success criteria are clear enough to evaluate. They aren’t replacing your deterministic code.</p><h3>Where This Actually Works</h3><p>We had thousands of configuration files for web scraping. Giant instruction sets telling our engine which page to enter, what to click, what to look for. Clients wanted to track how their products were performing across various sites. The problem was that websites change constantly. A button moves, some text updates, and suddenly dozens of scrapers break. We’d spend days tracking down failures and updating configs.</p><p>Then we built a tool where you just specify what you’re looking for. No step-by-step instructions. The agent scouts the site until it finds what’s needed. Sites can redesign their entire layout overnight, and the next day the scraper just figures it out.</p><p>This worked because the problem had the right architecture. The success criteria were clear (find this specific data). The execution was self-contained (one site at a time). Verification was straightforward (did you get the data or not). Not every scraping problem has this structure, but the ones that do benefit dramatically from this approach.</p><h3>What’s Still Hard</h3><p>The biggest challenge is definitional, not technical. You can’t just say “make good decisions” and expect an agent to know what “good” means in your context. You need to be precise about priorities, edge cases that matter, when to be conservative versus aggressive.</p><p>I’ve watched agents do something technically correct based on the guidelines we gave them, and immediately known we’d explained our requirements wrong. We failed to understand what we actually wanted clearly enough. The agent just did what we asked.</p><p>The architectural questions matter more than people realize. When should you use agents versus deterministic code? When do you need multiple specialized agents instead of one general-purpose one? Where should the boundaries be? These are fundamental design decisions that determine whether the system works at all.</p><p>There’s also the reliability question. Deterministic code is testable in ways that agent-based systems aren’t. An agent might handle the same situation differently based on context you didn’t think was relevant. For many problems, that variability is the entire point. But you need to be comfortable with it, and you need monitoring and guardrails that match that reality.</p><h3>The Real Shift</h3><p>We’re moving from “predict every possible use case” to “explain the world clearly and define success.”</p><p>Traditional engineering is about anticipation. You think through scenarios, you code for them. Thorough, logical, and it works beautifully when the possibility space is manageable. For complex, dynamic environments (markets, user interactions, systems that change daily), the possibility space is effectively infinite.</p><p>With agents, you’re teaching instead of predicting. You explain how your world works, what the constraints are, what matters, what the tradeoffs look like. The agent navigates within that space, handling specific scenarios as they emerge in real time. It requires you to articulate things you might have just intuitively understood before. When it works, it handles situations that rigid code simply can’t.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Na4mzYRB5H_XUf_MD09fvQ.png" /></figure><h3>Moving Forward</h3><p>The puzzle between messy reality and deterministic code isn’t going away. We’ve never been able to predict the future, and that hasn’t changed. But we’re getting better at building systems that adapt rather than systems that try to anticipate everything.</p><p>Agents work well for a specific class of problems (ones where the solution space is too large to enumerate but success is definable enough to recognize). Understanding which problems have that structure, and how to architect systems that take advantage of it, separates production-ready agent systems from experiments that look impressive in demos.</p><p>The puzzle is still endless. But we’re learning to solve it differently. Instead of predicting every scenario, we’re building systems that can reason about the ones we didn’t predict. We’re just starting to understand what becomes possible when you stop trying to code for everything and start building systems that can figure it out.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lKUmUxldm5xXLWfGgmrOVw.png" /></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5e4e8c1dc882" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The AI Architecture Illusion: Why Generated Code Works in Demos but Dies in Production]]></title>
            <link>https://medium.com/similarweb-engineering/the-ai-architecture-illusion-why-generated-code-works-in-demos-but-dies-in-production-41c7d40557e9?source=rss-b56441e272bc------2</link>
            <guid isPermaLink="false">https://medium.com/p/41c7d40557e9</guid>
            <category><![CDATA[software-architecture]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[technical-debt]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Dor Amram]]></dc:creator>
            <pubDate>Tue, 19 Aug 2025 06:14:24 GMT</pubDate>
            <atom:updated>2025-08-19T06:14:24.639Z</atom:updated>
            <content:encoded><![CDATA[<p><em>AI can write flawless code that follows terrible patterns. Here’s how to spot the architectural anti-patterns that will haunt your system later.</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ISmJa_GGVl0mSncP-NT8gw.png" /><figcaption>The surface: AI-generated code that passes all tests and looks production-ready</figcaption></figure><p>Last month, I reviewed a pull request that made me do a double-take. The code was pristine — clean functions, comprehensive tests, clear variable names. It implemented a user notification system that worked perfectly in our staging environment.</p><p>Three weeks after production deployment, our support team was drowning in timeout errors. The elegant notification system was making 47 database calls per user action, the beautiful synchronous API was blocking threads for minutes, and the “comprehensive” error handling was silently swallowing failures that should have been escalated.</p><p>The code was AI-generated. And it was architecturally catastrophic.</p><p>This is the hidden danger of AI-assisted development: tools that write syntactically perfect code while making architectural decisions that will cripple your system at scale. Let me show you the patterns I’ve seen AI get consistently wrong — and how to guide it toward better solutions.</p><h3>Pattern #1: The CRUD Trap — When AI Misses Your Domain Model</h3><p><strong>What AI Does:</strong> AI loves CRUD. Give it a feature request, and it’ll default to Create, Read, Update, Delete operations wrapped in REST endpoints. It’s simple, predictable, and works for demos.</p><p><strong>The Problem:</strong> Your business domain isn’t CRUD operations. It’s workflows, state transitions, and business rules that evolve over time.</p><p><strong>Real Example:</strong> I asked an AI to implement an order management system. It generated this structure:</p><pre># AI&#39;s approach - pure CRUD<br>class Order:<br>  def __init__(self, customer_id, items):<br>    self.status = &quot;pending&quot;<br>    self.customer_id = customer_id<br>    self.items = items<br>  <br>  def update_status(self, new_status):<br>    self.status = new_status # Any status to any status<br>  <br>  def update_items(self, new_items):<br>    self.items = new_items # Even after shipping!</pre><p>Looks reasonable, right? But this design allows orders to jump from “shipped” back to “pending,” lets you modify items after payment, and has no concept of business rules or state validation.</p><p><strong>What Your Domain Actually Needs:</strong></p><pre># Domain-driven approach<br>class Order:<br>  def __init__(self, customer_id, items):<br>    self.customer_id = customer_id<br>    self.items = items<br>    self.status = OrderStatus.DRAFT<br>    self.events = []<br>  <br>  def submit_for_payment(self):<br>    if self.status != OrderStatus.DRAFT:<br>      raise InvalidTransition(&quot;Cannot submit non-draft order&quot;)<br>    self._transition_to(OrderStatus.PAYMENT_PENDING)<br>  <br>  def confirm_payment(self, payment_id):<br>    if self.status != OrderStatus.PAYMENT_PENDING:<br>      raise InvalidTransition(&quot;Cannot confirm payment for non-pending order&quot;)<br>    self._add_event(PaymentConfirmed(payment_id))<br>    self._transition_to(OrderStatus.CONFIRMED)<br>  <br>  def _transition_to(self, new_status):<br>    self.events.append(StatusChanged(self.status, new_status))<br>    self.status = new_status</pre><p><strong>Why AI Gets This Wrong:</strong> AI optimizes for implementation simplicity, not domain correctness. It doesn’t understand that your “order” represents a business process with rules, not just data storage.</p><p><strong>How to Guide AI:</strong> <br>Instead of: “<em>Create an order management system</em>” <br>Try: “<em>Model an order as a state machine with these valid transitions: draft→payment_pending→confirmed→shipped→delivered. Each transition should validate business rules and emit domain events</em>.”</p><h3>Pattern #2: The Synchronous Seduction — When AI Blocks Your Threads</h3><p><strong>What AI Does:</strong> AI defaults to synchronous patterns. Function calls return values directly, APIs wait for responses, operations complete before moving on. It’s easier to reason about and debug.</p><p><strong>The Problem:</strong> Modern applications are fundamentally asynchronous. Users don’t wait for batch processes, external services have variable latency, and blocking operations kill scalability.</p><p><strong>Real Example:</strong> I asked AI to implement a user onboarding flow:</p><pre># AI&#39;s synchronous approach<br>def onboard_user(user_data):<br>  # Create account<br>  user = create_user_account(user_data)<br>  <br>  # Send welcome email (blocks for 2–3 seconds)<br>  email_service.send_welcome_email(user.email)<br>  <br>  # Generate profile image (blocks for 5–10 seconds)<br>  avatar_url = image_service.generate_avatar(user.name)<br>  user.avatar_url = avatar_url<br>  user.save()<br><br>  # Sync with CRM (blocks for 3–5 seconds)<br>  crm_service.create_contact(user)<br>  <br>  # Setup analytics tracking (blocks for 1–2 seconds)<br>  analytics_service.track_signup(user.id)<br>  <br>  return user</pre><p>This “works,” but the user waits 10–20 seconds for account creation while your API threads are tied up with external service calls.</p><p><strong>What You Actually Need:</strong></p><pre># Event-driven asynchronous approach<br>async def onboard_user(user_data):<br>  # Critical path - must complete immediately<br>  user = await create_user_account(user_data)<br>  <br>  # Non-critical operations - emit events for background processing<br>  await event_bus.publish(UserSignedUp(<br>    user_id=user.id,<br>    email=user.email,<br>    name=user.name<br>  ))<br>  <br>  return user<br><br># Background handlers process events asynchronously<br>@event_handler(UserSignedUp)<br>async def handle_user_signup(event):<br>  tasks = [<br>    email_service.send_welcome_email(event.email),<br>    image_service.generate_avatar(event.name),<br>    crm_service.create_contact(event.user_id),<br>    analytics_service.track_signup(event.user_id)<br>  ]<br>  await asyncio.gather(*tasks, return_exceptions=True)</pre><p><strong>Why AI Gets This Wrong:</strong> AI optimizes for code that “just works” in isolation. It doesn’t consider concurrent users, service latency, or system-wide performance characteristics.</p><p><strong>How to Guide AI:</strong> <br>Instead of: “<em>Build a user onboarding system</em>” <br>Try: “<em>Design a user onboarding flow where account creation is synchronous but welcome emails, avatar generation, and CRM sync happen asynchronously via events. The user should see immediate feedback while background processes handle non-critical operations</em>.”</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*E7eUfdJqYQdO09G3o-kSOw.png" /><figcaption>Below the surface: Architectural decisions that will cripple your system at scale</figcaption></figure><h3>Pattern #3: The Error Handling Mirage — When AI Optimizes for the Happy Path</h3><p><strong>What AI Does:</strong> AI writes error handling that looks comprehensive but misses the errors that actually matter in production.</p><p><strong>The Problem:</strong> AI focuses on syntax errors and type mismatches, but real systems fail in ways that are hard to predict: network partitions, partial failures, cascading timeouts, and race conditions.</p><p><strong>Real Example:</strong> AI-generated payment processing code:</p><pre># AI&#39;s &quot;comprehensive&quot; error handling<br>def process_payment(order_id, payment_data):<br>  try:<br>    order = get_order(order_id)<br>    if not order:<br>      return {&quot;error&quot;: &quot;Order not found&quot;}<br>    <br>    payment_result = payment_gateway.charge(<br>      amount=order.total,<br>      card=payment_data[&#39;card&#39;]<br>    )<br>    <br>    if payment_result.success:<br>      order.status = &quot;paid&quot;<br>      order.save()<br>      return {&quot;success&quot;: True, &quot;transaction_id&quot;: payment_result.id}<br>    else:<br>      return {&quot;error&quot;: payment_result.error_message}<br>  <br>  except Exception as e:<br>    logger.error(f&quot;Payment processing failed: {e}&quot;)<br>    return {&quot;error&quot;: &quot;Payment processing failed&quot;}</pre><p>This handles the obvious cases but creates subtle data corruption:</p><ol><li>What if the payment succeeds but saving the order fails?</li><li>What if the payment gateway returns success but then sends a webhook saying it failed?</li><li>What if this function runs twice for the same order due to a retry?</li></ol><p><strong>What You Actually Need:</strong></p><pre># Robust error handling with compensation patterns<br>def process_payment(order_id, payment_data, idempotency_key):<br>  # Check if we&#39;ve already processed this request<br>  existing_result = get_cached_result(idempotency_key)<br>  if existing_result:<br>    return existing_result<br>  try:<br>    # Start distributed transaction<br>    with transaction_manager() as tx:<br>      order = get_order_for_update(order_id)<br>    if order.status != &quot;pending_payment&quot;:<br>      return {&quot;error&quot;: &quot;Order not in payable state&quot;}<br>    <br>    # Reserve inventory before charging<br>    if not inventory_service.reserve_items(order.items, tx):<br>      return {&quot;error&quot;: &quot;Items no longer available&quot;}<br>    <br>    # Process payment with compensation logic<br>    payment_result = payment_gateway.charge(<br>      amount=order.total,<br>      card=payment_data[&#39;card&#39;],<br>      idempotency_key=idempotency_key<br>    )<br><br>    if payment_result.success:<br>      order.status = &quot;paid&quot;<br>      order.payment_id = payment_result.id<br>      order.save()<br>      <br>      # Schedule payment confirmation check<br>      schedule_payment_verification(payment_result.id, delay=300)<br>      result = {&quot;success&quot;: True, &quot;transaction_id&quot;: payment_result.id}<br>      cache_result(idempotency_key, result)<br>      return result<br>    <br>    else:<br>      # Release reserved inventory<br>      inventory_service.release_items(order.items, tx)<br>      return {&quot;error&quot;: payment_result.error_message}<br>  <br>  except PaymentGatewayTimeout:<br>    # Payment might have succeeded - check status<br>    schedule_payment_status_check(order_id, idempotency_key)<br>    return {&quot;error&quot;: &quot;Payment processing - status will be confirmed shortly&quot;}<br>  <br>  except Exception as e:<br>    # Ensure any partial state is cleaned up<br>    cleanup_partial_payment_state(order_id, idempotency_key)<br>    logger.error(f&quot;Payment processing failed: {e}&quot;, extra={&quot;order_id&quot;: order_id})<br>    return {&quot;error&quot;: &quot;Payment processing failed&quot;}</pre><p><strong>Why AI Gets This Wrong:</strong> AI doesn’t understand distributed systems failure modes. It optimizes for code that handles expected exceptions, not systems that maintain consistency under partial failures.</p><p><strong>How to Guide AI:</strong> <br>Instead of: “<em>Add error handling to this payment processing function</em>” <br>Try: “<em>Design payment processing that handles these failure scenarios: payment succeeds but order update fails, gateway timeout with unknown payment status, duplicate payment attempts, and inventory changes between payment and confirmation. Use compensation patterns.</em>”</p><h3>Pattern #4: The Database Relationship Disaster — When AI Normalizes Everything</h3><p><strong>What AI Does:</strong> AI loves clean, normalized database schemas. Every entity gets its own table, every relationship is properly foreign-keyed, and the ERD looks like a textbook example.</p><p><strong>The Problem:</strong> Normalized schemas optimize for write consistency but can create query performance disasters. AI doesn’t understand your read patterns, query volume, or consistency requirements.</p><p><strong>Real Example:</strong> AI-generated e-commerce schema:</p><pre>-- AI&#39;s &quot;perfect&quot; normalization<br>CREATE TABLE products (<br>  id INT PRIMARY KEY,<br>  name VARCHAR(255),<br>  description TEXT<br>);<br><br>CREATE TABLE categories (<br>  id INT PRIMARY KEY,<br>  name VARCHAR(255)<br>);<br><br>CREATE TABLE warehouses (<br>  id INT PRIMARY KEY,<br>  name VARCHAR(255),<br>  location VARCHAR(255)<br>);<br><br>CREATE TABLE product_categories (<br>  product_id INT REFERENCES products(id),<br>  category_id INT REFERENCES categories(id)<br>);<br><br>CREATE TABLE product_attributes (<br>  id INT PRIMARY KEY,<br>  product_id INT REFERENCES products(id),<br>  attribute_name VARCHAR(255),<br>  attribute_value VARCHAR(255)<br>);<br><br>CREATE TABLE inventory (<br>  id INT PRIMARY KEY,<br>  product_id INT REFERENCES products(id),<br>  warehouse_id INT REFERENCES warehouses(id),<br>  quantity INT<br>);</pre><p>This schema requires 5+ joins for basic product listing queries and becomes unusable at scale.</p><p><strong>What Your Read Patterns Actually Need:</strong></p><pre>-- Read-optimized with strategic denormalization<br>CREATE TABLE products (<br>  id INT PRIMARY KEY,<br>  name VARCHAR(255),<br>  description TEXT,<br>  category_names TEXT[], -- Denormalized for filtering<br>  attributes JSONB, -- Flexible attributes in one place<br>  total_inventory INT, -- Cached across warehouses<br>  inventory_by_warehouse JSONB, -- When you need details<br>  search_vector tsvector, -- Full-text search<br>  last_updated TIMESTAMP<br>);<br><br>-- Separate write-optimized tables for updates<br>CREATE TABLE inventory_events (<br>  id BIGSERIAL PRIMARY KEY,<br>  product_id INT,<br>  warehouse_id INT,<br>  quantity_change INT,<br>  event_time TIMESTAMP<br>);</pre><p><strong>Why AI Gets This Wrong:</strong> AI applies database normalization rules without understanding query patterns, read-write ratios, or consistency requirements specific to your domain.</p><p><strong>How to Guide AI:</strong> <br>Instead of: “<em>Design a database schema for e-commerce</em>” <br>Try: “<em>Design a schema optimized for read-heavy product catalogs where we need fast filtering by category and attributes. Write operations happen via events, and we can tolerate eventual consistency for inventory counts. Optimize for sub-100ms product listing queries</em>.”</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bwiRft1NTHa5yXC5i-iTgA.png" /><figcaption>The wreckage: When architectural debt finally sinks your production system</figcaption></figure><h3>Pattern #5: The Testing Theater — When AI Tests Implementation, Not Behavior</h3><p><strong>What AI Does:</strong> AI generates comprehensive unit tests that achieve high coverage by testing implementation details rather than business behavior.</p><p><strong>The Problem:</strong> Implementation-focused tests become maintenance burdens that don’t catch real bugs. They pass when your code is broken and fail when you refactor correctly.</p><p><strong>Real Example:</strong> AI-generated tests for a shopping cart:</p><pre># AI&#39;s implementation-focused tests<br>def test_add_item_to_cart():<br>  cart = ShoppingCart()<br>  item = CartItem(product_id=1, quantity=2, price=10.00)<br>  cart.add_item(item)<br>  <br>  # Testing internal state instead of behavior<br>  assert len(cart._items) == 1<br>  assert cart._items[0].product_id == 1<br>  assert cart._items[0].quantity == 2<br>  assert cart._total == 20.00<br> <br>def test_cart_calculates_total():<br>  cart = ShoppingCart()<br>  <br>  # Tests the specific calculation method<br>  cart._items = [<br>    CartItem(product_id=1, quantity=2, price=10.00),<br>    CartItem(product_id=2, quantity=1, price=5.00)<br>  ]<br>  <br>  total = cart._calculate_total()<br>  assert total == 25.00</pre><p>These tests break when you change internal implementation and don’t verify actual user-facing behavior.</p><p><strong>What You Actually Need:</strong></p><pre># Behavior-focused tests<br>def test_user_can_add_items_and_see_correct_total():<br>  cart = ShoppingCart()<br>  <br>  # Test the behavior users care about<br>  cart.add_product(product_id=1, quantity=2, unit_price=10.00)<br>  cart.add_product(product_id=2, quantity=1, unit_price=5.00)<br>  <br>  # Verify observable behavior, not implementation<br>  assert cart.get_total() == 25.00<br>  assert cart.get_item_count() == 3<br>  assert cart.contains_product(1)<br>  assert cart.contains_product(2)<br><br>def test_cart_handles_inventory_constraints():<br>  cart = ShoppingCart()<br>  inventory = MockInventoryService()<br>  inventory.set_available_quantity(product_id=1, quantity=1)<br>  <br>  # Test business rule enforcement<br>  with pytest.raises(InsufficientInventoryError):<br>    cart.add_product(product_id=1, quantity=2, inventory_service=inventory)<br><br>def test_cart_preserves_items_across_sessions():<br>  # Test the behavior that matters to users<br>  user_id = &quot;user123&quot;<br>  <br>  cart1 = ShoppingCart.load_for_user(user_id)<br>  cart1.add_product(product_id=1, quantity=1, unit_price=10.00)<br>  cart1.save()<br>  <br>  cart2 = ShoppingCart.load_for_user(user_id)<br>  <br>  assert cart2.get_total() == 10.00</pre><p><strong>Why AI Gets This Wrong:</strong> AI tests the code it generated, not the behavior your users depend on. It doesn’t understand which aspects of your system are essential contracts versus implementation details.</p><p><strong>How to Guide AI:</strong> <br>Instead of: “<em>Write unit tests for this shopping cart class</em>” <br>Try: “<em>Write tests that verify these user behaviors: adding items updates the total correctly, quantity constraints are enforced, cart contents persist across sessions, and duplicate items are handled appropriately. Focus on testing the public interface, not internal methods</em>.”</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KCuoR4qzEt9zLkgIlq1hwQ.png" /><figcaption>The solution: Navigate with architectural awareness, not just surface-level code quality</figcaption></figure><h3>The Path Forward: Architectural AI Partnership</h3><p>The solution isn’t to avoid AI — it’s to guide it toward better architectural decisions. Here’s how:</p><p><strong>1. Lead with Constraints, Not Features</strong> Instead of describing what you want to build, describe the forces your system must handle: concurrency, consistency requirements, performance characteristics, and failure modes.</p><p><strong>2. Specify Non-Functional Requirements</strong> AI optimizes for functional correctness. You must explicitly specify scalability, maintainability, and reliability requirements.</p><p><strong>3. Review Generated Code for Architectural Patterns</strong> Ask yourself: Does this code make assumptions about usage patterns? How would this behave under load? What happens when external dependencies fail?</p><p><strong>4. Use AI for Exploration, Humans for Decisions</strong> Let AI generate multiple approaches, but use human judgment to evaluate tradeoffs and select patterns that fit your specific context.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WPR_3penA7owxENjWBe1YA.png" /><figcaption>The greatest illusion in software engineering: code that works perfectly in demos but crumbles under production reality</figcaption></figure><p>The developers who thrive with AI won’t be those who generate code fastest — they’ll be those who can spot architectural anti-patterns and guide AI toward solutions that survive contact with production reality.</p><p>Your system’s architecture is too important to delegate to a tool that optimizes for demos instead of durability. Use AI as a powerful implementation partner, but keep the architectural decisions where they belong: with engineers who understand the difference between code that works and code that lasts.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=41c7d40557e9" width="1" height="1" alt=""><hr><p><a href="https://medium.com/similarweb-engineering/the-ai-architecture-illusion-why-generated-code-works-in-demos-but-dies-in-production-41c7d40557e9">The AI Architecture Illusion: Why Generated Code Works in Demos but Dies in Production</a> was originally published in <a href="https://medium.com/similarweb-engineering">Similarweb Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The ChatGPT Trap: Why Junior Developers Are Learning Less While Building More]]></title>
            <link>https://medium.com/similarweb-engineering/the-chatgpt-trap-why-junior-developers-are-learning-less-while-building-more-49b863dca532?source=rss-b56441e272bc------2</link>
            <guid isPermaLink="false">https://medium.com/p/49b863dca532</guid>
            <category><![CDATA[learning]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[best-practices]]></category>
            <category><![CDATA[personal-growth]]></category>
            <dc:creator><![CDATA[Dor Amram]]></dc:creator>
            <pubDate>Mon, 11 Aug 2025 16:04:21 GMT</pubDate>
            <atom:updated>2025-08-11T16:04:21.018Z</atom:updated>
            <content:encoded><![CDATA[<p>I watched a junior developer spend three hours “coding” a feature last week. Copy from ChatGPT, paste into the IDE, fix the syntax error, push to staging. Feature complete, ticket closed, sprint velocity maintained.</p><p>The feature broke in production two days later.</p><p>When asked to debug it, the developer stared at the code like it was written in hieroglyphics. Because in a way, it was. They had built something they didn’t understand, solving a problem they never truly grasped.</p><p><strong>This is the ChatGPT Trap, and it’s stealing the learning experiences that create great developers.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nbsEu3eoLH6m-6wp2k5I1A.png" /><figcaption>‘ChatGPT is my senior developer’ vs. ‘ChatGPT is my really smart intern’</figcaption></figure><h3>The Invisible Skills We’re Not Building</h3><p>Here’s what I see happening across engineering teams:</p><p><strong>The Assignment Dump</strong>: “Here’s my ticket, ChatGPT. Solve it.” No problem breakdown, no requirement analysis, no architectural thinking. Just hoping AI will figure out what they couldn’t be bothered to understand.</p><p><strong>The Copy-Paste Cycle</strong>: Entire functions copied wholesale, variable names unchanged, patterns unrecognized. When the code inevitably needs modification, they’re back to ChatGPT for another complete rewrite.</p><p><strong>The Question Drought</strong>: Instead of asking clarifying questions about requirements or exploring edge cases, developers let AI make those decisions. The problem? AI doesn’t know your business context, your user needs, or your system constraints.</p><h3>Technology Problems Are Rarely Technology Problems</h3><p>Here’s the truth that’s getting lost in the AI hype: <strong>Your biggest technical debt isn’t inefficient algorithms or outdated frameworks. It’s misunderstood requirements.</strong></p><p>That “simple” user authentication feature? It touches session management, security policies, user experience flows, error handling, and data privacy requirements. ChatGPT can generate the code, but it can’t tell you which business rules you forgot to ask about.</p><p>The most expensive line of code is the one that solves the wrong problem. And if you don’t understand the problem deeply enough to debug, modify, or extend your solution, you’ve built technical debt with AI assistance.</p><h3>AI Gives You Faster Answers, But Are You Asking the Right Questions?</h3><p>AI is incredibly powerful, but it amplifies whatever approach you bring to it. Bring shallow thinking, get shallow solutions. Bring deep problem understanding, get genuinely helpful assistance.</p><p><strong>Instead of:</strong> “Build me a REST API for user management” <strong>Try:</strong> “I’m designing user management for a multi-tenant application. What are the key architectural decisions I should consider around data isolation, authentication, and scalability? Help me think through the tradeoffs.”</p><p><strong>Instead of:</strong> “Fix this bug” <em>[paste entire function]</em> <strong>Try:</strong> “This function should handle user input validation, but it’s failing on edge cases. Help me understand why this approach might be fragile and what validation patterns would be more robust.”</p><p>See the difference? One treats AI as a solution machine. The other treats it as a thinking partner.</p><h3>The Questions That Make AI Your Learning Partner</h3><p>Before you open ChatGPT, ask yourself:</p><ul><li>What problem am I really trying to solve?</li><li>What do I already know about this domain?</li><li>What specific part am I stuck on?</li><li>What would success look like from a user perspective?</li></ul><p>When working with AI, prioritize understanding:</p><ul><li>“Explain this concept, don’t just give me code”</li><li>“What are the tradeoffs of this approach?”</li><li>“What edge cases should I consider?”</li><li>“How would you test this solution?”</li></ul><p>After AI gives you code, dig deeper:</p><ul><li>“Why did you choose this pattern?”</li><li>“What would break if the requirements changed?”</li><li>“How would I modify this for different use cases?”</li></ul><h3>The 24-Hour Test</h3><p>Here’s how to know if you’re learning or just producing:</p><p>Can you explain the code you wrote yesterday without looking at it? Can you modify it for a new requirement? Can you debug it when it breaks?</p><p>If the answer is no, you’re letting AI learn instead of you.</p><h3>Building Systems That Survive Contact With Reality</h3><p>The best developers I work with use AI to accelerate their learning, not replace their thinking. They understand that:</p><ul><li><strong>Good architecture starts with good questions, not good code</strong></li><li><strong>Your users don’t care about your stack, they care about their problems</strong></li><li><strong>Every feature request is really a hypothesis that needs testing</strong></li></ul><p>They use AI to explore possibilities, understand tradeoffs, and generate options. But they make the decisions, understand the implications, and take responsibility for the outcomes.</p><h3>The Path Forward</h3><p>AI isn’t going anywhere, and neither should your curiosity. The developers who thrive in the next decade will be those who can leverage AI’s capabilities while maintaining their problem-solving skills.</p><p>Use AI as a research assistant, not a replacement brain. Let it help you explore solutions faster, but never let it do your thinking for you.</p><p>Because at the end of the day, the code that matters most isn’t the code that works once. It’s the code you can debug, extend, and evolve as your understanding of the problem deepens.</p><p><strong>Your users are debugging your assumptions every day. Make sure you understand those assumptions well enough to fix them.</strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=49b863dca532" width="1" height="1" alt=""><hr><p><a href="https://medium.com/similarweb-engineering/the-chatgpt-trap-why-junior-developers-are-learning-less-while-building-more-49b863dca532">The ChatGPT Trap: Why Junior Developers Are Learning Less While Building More</a> was originally published in <a href="https://medium.com/similarweb-engineering">Similarweb Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Effective LLM Prompting: Getting the Code You Actually Need]]></title>
            <link>https://medium.com/similarweb-engineering/effective-llm-prompting-getting-the-code-you-actually-need-8d5c2cf12503?source=rss-b56441e272bc------2</link>
            <guid isPermaLink="false">https://medium.com/p/8d5c2cf12503</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[best-practices]]></category>
            <category><![CDATA[prompt-engineering]]></category>
            <category><![CDATA[coding]]></category>
            <dc:creator><![CDATA[Dor Amram]]></dc:creator>
            <pubDate>Mon, 31 Mar 2025 19:14:44 GMT</pubDate>
            <atom:updated>2025-03-31T19:14:44.236Z</atom:updated>
            <content:encoded><![CDATA[<p>As software engineers, we’ve quickly adapted to using Large Language Models (LLMs) like ChatGPT, Claude, and others as part of our development workflow. Yet many of us have experienced that familiar frustration: you ask for a specific piece of code, only to receive something that’s <em>almost</em> right but requires significant reworking. The good news? With better prompting techniques, you can dramatically improve your results and efficiency.</p><p>Drawing from my experience as both a software developer and LLM user, I’ve compiled these battle-tested practices that transform how effectively you can leverage these AI tools for coding tasks. Think of this as your practical field guide to speaking the language that gets you the code you actually need.</p><h3>Understanding the “Mind” Behind the Model</h3><p>Before diving into specific techniques, it helps to understand how LLMs approach coding tasks. Unlike human programmers, LLMs don’t truly “understand” code in the way we do. They recognize patterns and predict what should come next based on their training data.</p><p>This pattern-matching approach means that your prompt serves as both the problem statement and an implicit guide for how the solution should be structured. When your prompt lacks clarity or specificity, the model fills in the gaps with what it considers most probable based on similar examples in its training — which may not align with your actual needs.</p><h3>Best Practice #1: Set the Context with Precision</h3><p>One of the most common mistakes I see is jumping straight to requesting a specific function or feature without establishing the broader context. Just as you wouldn’t expect a new team member to write perfect code without understanding your project, LLMs need context to generate appropriate solutions.</p><p><strong>Why this matters</strong>: Context helps the LLM understand the environment where your code will live, the constraints it must operate within, and the patterns it should follow. Without this information, the model may produce technically correct code that’s completely misaligned with your actual project needs.</p><p><strong>Instead of this</strong>:</p><pre>Write a function that validates a user&#39;s password.</pre><p><strong>Try this</strong>:</p><pre>I&#39;m working on a Node.js application using Express and MongoDB. We follow a service-oriented architecture with separate validation utilities. I need a password validation function that:<br>- Checks for minimum 8 characters<br>- Requires at least one uppercase letter, one lowercase letter, and one number<br>- Doesn&#39;t allow more than 3 consecutive identical characters<br>- Returns a detailed object with a success flag and specific validation errors</pre><p>Notice how the improved prompt establishes the technical ecosystem, architectural pattern, and specific requirements. This context drastically narrows the solution space, leading to more relevant output.</p><h3>Best Practice #2: Use Test Cases as Specifications</h3><p>When describing what you want code to do, natural language can be surprisingly ambiguous. I’ve found that including explicit test cases in your prompt is one of the most effective ways to clarify your expectations.</p><p><strong>Why this matters</strong>: Test cases provide concrete examples of inputs and expected outputs, which eliminates ambiguity and helps the LLM understand edge cases. This approach also naturally aligns with test-driven development practices.</p><p><strong>Instead of this</strong>:</p><pre>Write a function that formats phone numbers.</pre><p><strong>Try this</strong>:</p><pre>Write a JavaScript function that formats phone numbers. The function should:<br>- Accept strings with 10 digits in any format<br>- Return a string formatted as (XXX) XXX-XXXX<br><br>Examples:<br>- formatPhoneNumber(&quot;1234567890&quot;) should return &quot;(123) 456–7890&quot;<br>- formatPhoneNumber(&quot;123–456–7890&quot;) should return &quot;(123) 456–7890&quot;<br>- formatPhoneNumber(&quot;(123) 456–7890&quot;) should return &quot;(123) 456–7890&quot;<br>- formatPhoneNumber(&quot;abc1234567890&quot;) should return &quot;Invalid input&quot;<br>- formatPhoneNumber(&quot;12345&quot;) should return &quot;Invalid input&quot;</pre><p>By providing these test cases, you’ve essentially defined a specification through examples, which dramatically increases the chances of getting exactly what you need on the first try.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*vQTMT9uWJe1h59by" /></figure><h3>Best Practice #3: Establish Your Coding Standards</h3><p>Each team and project has its own coding conventions and preferred patterns. By explicitly stating these preferences in your prompt, you can get code that feels like it was written by you or your team.</p><p><strong>Why this matters</strong>: Code that follows your established patterns integrates more seamlessly with your existing codebase and requires less refactoring. This saves time and reduces the cognitive load of switching between different styles.</p><p><strong>Instead of this</strong>:</p><pre>Create a React component for a user profile.</pre><p><strong>Try this</strong>:</p><pre>Create a React functional component for a user profile with the following guidelines:<br>- Use TypeScript with explicit interface for props<br>- Follow our naming convention: [ComponentName]/[ComponentName].tsx<br>- Use CSS modules for styling<br>- Use React hooks for any state management<br>- Include proper error handling for API data<br>- Add JSDoc comments for the component and its props<br>The component should display the user&#39;s profile image, name, email, and a list of their recent activities.</pre><p>This approach guides the LLM to produce code that aligns with your team’s practices, making the output immediately more valuable and reducing the need for adjustments.</p><h3>Best Practice #4: Request Error Handling and Edge Cases</h3><p>In my experience, one of the biggest gaps in LLM-generated code is proper error handling. The “happy path” usually works fine, but real-world code needs to gracefully handle all sorts of unexpected inputs and conditions.</p><p><strong>Why this matters</strong>: Production-quality code needs to be robust against invalid inputs, network failures, and other edge cases. Explicitly requesting this coverage prevents you from having to add it yourself later.</p><p><strong>Instead of this</strong>:</p><pre>Write a function to fetch user data from an API.</pre><p><strong>Try this</strong>:</p><pre>Write an async JavaScript function to fetch user data from an API with the following requirements:<br>- Use fetch API with proper timeout (5 seconds)<br>- Handle network errors gracefully<br>- Handle 404 errors by returning null<br>- Handle other HTTP errors by throwing a custom error with the status code<br>- Include retry logic (maximum 3 attempts with exponential backoff)<br>- Add TypeScript typings for the return value and parameters<br>The function should take a userId parameter and return a promise that resolves to the user object.</pre><p>By specifying your error handling expectations, you prompt the LLM to consider these critical aspects rather than focusing solely on the basic functionality.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*vv9d9d_FSdcf4Jsw" /></figure><h3>Best Practice #5: Break Down Complex Requests</h3><p>When you need complex functionality, resist the urge to request everything in a single prompt. Instead, break it down into logical components, just as you would decompose a complex programming task.</p><p><strong>Why this matters</strong>: LLMs perform better on focused, specific tasks rather than broad, multi-faceted ones. Breaking down requests helps the model concentrate on one aspect at a time, leading to higher-quality output for each piece.</p><p><strong>Instead of this</strong>:</p><pre>Create a full authentication system with login, registration, password reset, and profile management.</pre><p><strong>Try this</strong>:</p><p>A series of separate, focused prompts:</p><pre>1. &quot;Create a user schema and model for MongoDB with fields for authentication…&quot;</pre><pre>2. &quot;Write a registration controller function that validates input and creates a new user…&quot;</pre><pre>3. &quot;Create a JWT authentication middleware function that verifies tokens…&quot;</pre><p>This sequential approach mirrors how you would actually build such a system, focusing on one component at a time and ensuring each piece is solid before moving on.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*B_r5iZPTNvFi-mRx" /></figure><h3>Best Practice #6: Request Explanations and Comments</h3><p>When working with complex algorithms or domain-specific code, ask the LLM to explain its implementation. This not only helps you understand the generated code but often improves its quality as well.</p><p><strong>Why this matters</strong>: The act of explaining code forces clearer thinking — a principle known as the “rubber duck debugging” effect. When an LLM includes explanations, it often catches issues in its own logic and produces better organized, more thoughtful code.</p><p><strong>Instead of this</strong>:</p><pre>Write a function to implement the Boyer-Moore string search algorithm.</pre><p><strong>Try this</strong>:</p><pre>Write a function to implement the Boyer-Moore string search algorithm with the following:<br>- Include detailed comments explaining each step of the algorithm<br>- Add a brief explanation of the time and space complexity<br>- Include examples of how the algorithm works on specific inputs<br>- Add TypeScript type annotations<br><br>After the code, please explain the key insight of the Boyer-Moore algorithm and why it&#39;s often more efficient than naive string searching.</pre><p>The explanations you receive serve as built-in documentation and help validate that the code is implementing what you expect.</p><h3>Best Practice #7: Iterative Refinement with Context</h3><p>Perhaps the most powerful technique is approaching complex tasks as a conversation rather than a one-shot request. Start with a basic implementation, then iteratively refine it while maintaining the full context.</p><p><strong>Why this matters</strong>: This approach mimics the natural development process and allows you to build on previous work rather than starting from scratch with each refinement. It also helps the LLM maintain consistency across iterations.</p><p><strong>A sample conversation flow</strong>:</p><ol><li>Initial request: “Create a basic Redux slice for managing user authentication state…”</li><li>Refinement: “Great, now add action creators for handling login failure scenarios…”</li><li>Further refinement: “Update the reducer to handle token refresh…”</li></ol><p>Each step builds on the previous one, with the LLM maintaining awareness of the evolving codebase.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*bekhmdtRKj8jwFNJ" /></figure><h3>Best Practice #8: Specify the Level of Abstraction</h3><p>Different coding tasks require different levels of abstraction. Being explicit about whether you want high-level pseudocode, detailed implementation, or something in between helps set appropriate expectations.</p><p><strong>Why this matters</strong>: Sometimes you need a detailed implementation ready to copy into your project; other times, you’re looking for a conceptual approach or architecture. Specifying this upfront helps you get exactly what you need.</p><p><strong>Example</strong>:</p><pre>I&#39;m designing a real-time notification system for a web application. <br>First, provide a high-level system architecture diagram showing the main components. <br>Then, focus on the database schema for storing notification preferences and history. <br>Finally, give me the detailed implementation for the notification dispatcher service in Node.js.</pre><p>This multi-level approach helps you establish the big picture before diving into implementation details.</p><h3>Putting It All Together: A Practical Workflow</h3><p>Based on these practices, here’s a workflow I’ve found effective for using LLMs in coding tasks:</p><ol><li><strong>Start with context</strong>: Begin by explaining your project, tech stack, and coding patterns.</li><li><strong>Define requirements clearly</strong>: Use test cases, examples, and explicit criteria.</li><li><strong>Request a specific scope</strong>: Ask for focused functionality rather than entire systems.</li><li><strong>Review and refine</strong>: Treat the first response as a starting point, then iterate with specific feedback.</li><li><strong>Ask for explanations</strong>: Request the LLM to walk through complex parts of the generated code.</li><li><strong>Integrate mindfully</strong>: Always review generated code for security, performance, and alignment with best practices.</li></ol><h3>Conclusion: Prompting as a Programming Skill</h3><p>Effective LLM prompting for coding tasks is emerging as a distinct skill — one that bridges natural language communication and programming. By applying these techniques consistently, you’ll find that your interactions with AI coding assistants become increasingly productive, allowing you to focus more on the creative and architectural aspects of development while delegating routine coding tasks.</p><p>Remember that these models are continuously evolving. The techniques that work best today may need adjustment as capabilities advance. However, the fundamental principle remains: the more effectively you communicate your requirements and context, the better results you’ll achieve.</p><p>Treat your AI coding assistant as a junior developer with impressive capabilities but who needs clear guidance — and you’ll be amazed at how much more productive your partnership becomes.</p><p><em>About the author: This article was written by a software engineer with extensive experience in both software development and leveraging LLMs to enhance coding productivity.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8d5c2cf12503" width="1" height="1" alt=""><hr><p><a href="https://medium.com/similarweb-engineering/effective-llm-prompting-getting-the-code-you-actually-need-8d5c2cf12503">Effective LLM Prompting: Getting the Code You Actually Need</a> was originally published in <a href="https://medium.com/similarweb-engineering">Similarweb Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building on Solid Ground: The Critical Importance of Good Architecture for LLM Tools]]></title>
            <link>https://medium.com/similarweb-engineering/building-on-solid-ground-the-critical-importance-of-good-architecture-for-llm-tools-b54cb80ef338?source=rss-b56441e272bc------2</link>
            <guid isPermaLink="false">https://medium.com/p/b54cb80ef338</guid>
            <category><![CDATA[architecture]]></category>
            <category><![CDATA[llm-agent]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[design]]></category>
            <category><![CDATA[agents]]></category>
            <dc:creator><![CDATA[Dor Amram]]></dc:creator>
            <pubDate>Thu, 06 Mar 2025 07:17:22 GMT</pubDate>
            <atom:updated>2025-03-06T07:17:22.395Z</atom:updated>
            <content:encoded><![CDATA[<p>In the fast-paced world of AI development, we often find ourselves chasing the latest breakthroughs and capabilities. But beneath the excitement of new models and features lies something far less glamorous yet infinitely more important: architecture. As a tech lead tasked with building AI infrastructure to serve an entire organization, I’ve come to realize that solid architectural foundations aren’t just nice to have — they’re absolutely essential for long-term success.</p><h3>Why Architecture Matters Now More Than Ever</h3><p>Imagine you’re building a house. You could focus all your energy on selecting beautiful furniture, state-of-the-art appliances, and perfect paint colors. But if the foundation is weak or the frame is unstable, those details won’t matter — the whole structure is at risk.</p><p>The same principle applies when building systems around Large Language Models (LLMs). The technology is evolving at breakneck speed, with new models, techniques, and tools emerging almost weekly. In this environment, focusing solely on today’s capabilities without considering tomorrow’s needs is a recipe for technical debt and frustration.</p><p>Good architecture isn’t just about making things work today — it’s about building systems that can adapt, scale, and evolve as the technology landscape shifts beneath our feet.</p><h3>The Four Pillars of Effective LLM Architecture</h3><p>Through experience and sometimes painful lessons, I’ve identified four core principles that form the foundation of effective LLM architecture:</p><h3>1. Modularity: Building with LEGO, Not Concrete</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UI5BzHfOAaaF86HTezwD8A.png" /></figure><p>When working with LLM tools, modularity is your greatest ally. Each component in your system should have clear boundaries and well-defined interfaces. This approach allows you to:</p><ul><li>Replace individual components as better alternatives emerge</li><li>Experiment with new models or techniques without rebuilding the entire system</li><li>Create specialized workflows for different teams or use cases</li></ul><p>The LangChain ecosystem, which we’ve adopted as our foundation, embraces this modularity. It allows us to swap out embedding models, change vector stores, or update prompt templates without disrupting the entire system.</p><p>Consider this: Would you rather replace a single brick in your structure or rebuild the entire wall? Modularity gives you that flexibility.</p><h3>2. Observability: You Can’t Improve What You Can’t Measure</h3><p>LLMs can sometimes feel like black boxes. Without proper observability, you’re flying blind — unable to identify issues, measure improvements, or justify investments.</p><p>Effective LLM architecture must include robust mechanisms for:</p><ul><li>Logging inputs, outputs, and important intermediate steps</li><li>Tracking performance metrics (latency, token usage, costs)</li><li>Evaluating result quality against defined benchmarks</li><li>Identifying patterns in failures or suboptimal outputs</li></ul><p>Dedicated observability platforms provide this crucial visibility layer, much like having a dashboard in your car that shows engine temperature, fuel levels, and speed. This visibility into what’s happening at each step of your LLM pipelines becomes invaluable when debugging issues, optimizing performance, or demonstrating value to stakeholders.</p><h3>3. Adaptability: Planning for Change, Not Fighting Against It</h3><p>The only constant in AI development is change. New models emerge, existing models improve, best practices evolve, and organizational needs shift. Your architecture shouldn’t just accommodate change — it should embrace it.</p><p>Think of it like building a garden rather than a concrete structure. Gardens are designed to grow and evolve with the seasons. In the same way, adaptable AI architecture allows for natural growth and change.</p><p>This means:</p><ul><li>Avoiding tight coupling to specific models or providers (like how a good garden isn’t dependent on just one type of plant)</li><li>Building abstraction layers that shield most of your codebase from underlying changes (similar to how mulch protects plant roots from temperature extremes)</li><li>Creating clear extension points for new capabilities (like leaving space in your garden for new plantings)</li><li>Designing with both backward compatibility and forward evolution in mind (ensuring new additions complement what’s already growing)</li></ul><p>Modern workflow orchestration systems demonstrate this adaptability beautifully. By defining workflows as graphs with clear transitions and state management, you create systems that can evolve piece by piece — like replacing individual plants in your garden rather than digging everything up and starting over.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VVezpHJVQ9w3rg-as2Ze0A.png" /></figure><h3>4. Governance: Balancing Innovation with Control</h3><p>As LLM usage grows within an organization, so does the need for governance. Without appropriate guardrails, you risk inconsistent experiences, runaway costs, or even compliance issues.</p><p>Effective LLM architecture must include:</p><ul><li>Centralized policy management for prompt engineering and model selection</li><li>Cost controls and usage monitoring</li><li>Authentication and authorization frameworks</li><li>Auditability for sensitive operations</li><li>Versioning and rollback capabilities for critical components</li></ul><p>This doesn’t mean stifling innovation — quite the opposite. Good governance creates safe spaces for experimentation while protecting production systems from unvetted changes.</p><h3>Real-World Architecture Patterns That Work</h3><p>Beyond these principles, several architectural patterns have proven effective when building LLM-powered systems:</p><h3>The Hub-and-Spoke Model</h3><p>In this pattern, a central orchestration layer manages access to LLM capabilities, while specialized adapters handle integration with various business systems. This creates a clean separation between:</p><ul><li>The core LLM interaction logic (prompting, chain execution, etc.)</li><li>The business-specific knowledge and integration points</li><li>The underlying model providers and tools</li></ul><p>This approach allows centralized teams (like yours) to maintain the core infrastructure while enabling individual departments to build their own specialized adapters.</p><h3>The Evaluation-First Pipeline</h3><p>Traditional software development emphasizes testing after implementation. With LLMs, this approach falls short because the outputs are probabilistic rather than deterministic.</p><p>An evaluation-first pipeline flips this model by:</p><ol><li>Defining clear evaluation metrics before building</li><li>Creating benchmark datasets that represent real-world usage</li><li>Establishing automated evaluation pipelines</li><li>Only then implementing and iterating on the actual solution</li></ol><p>This approach ensures that you can measure the impact of architectural changes on actual performance.</p><h3>The Feedback Loop Architecture</h3><p>LLMs improve with feedback. Your architecture should embrace this by creating explicit pathways for:</p><ul><li>Human feedback on model outputs</li><li>Automated detection of suboptimal responses</li><li>Continuous fine-tuning or retrieval augmentation based on this feedback</li><li>Performance monitoring to ensure improvements actually help</li></ul><p>This creates a virtuous cycle where your systems get better with use, rather than degrading over time.</p><h3>Common Pitfalls to Avoid</h3><p>Having worked on LLM infrastructure for our organization, I’ve witnessed several architectural missteps that create long-term problems:</p><h3>The Model-Centric Fallacy</h3><p>It’s tempting to build your architecture around the capabilities of a specific model. This creates brittle systems that break when model APIs change or when new, better models emerge.</p><p>Instead, design around the problems you’re solving, creating abstraction layers that can accommodate different models as appropriate.</p><h3>The Prompt Engineering Spaghetti</h3><p>Without architectural discipline, prompt engineering quickly becomes unmanageable — with critical business logic embedded in strings scattered throughout your codebase.</p><p>Treat prompts as first-class citizens in your architecture, with appropriate versioning, testing, and abstraction.</p><h3>The Integration Overload</h3><p>As LLMs prove their value, teams inevitably want to integrate them with everything — from knowledge bases to CRMs to internal tools.</p><p>Without a thoughtful integration architecture, you’ll find yourself building and maintaining countless one-off connectors. Instead, create standardized integration patterns and reusable components.</p><h3>The Observability Afterthought</h3><p>Too often, monitoring, logging, and evaluation are added after problems arise. By then, you’re already flying blind, unable to diagnose issues or measure improvements.</p><p>Bake observability into your architecture from day one. Luckily, we have great tools today that make this job easy, but the architectural commitment must come first.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xMRQxlQMGaSiiNEaa-hb2w.png" /></figure><h3>Building for an Uncertain Future</h3><p>Perhaps the greatest challenge in LLM architecture is the uncertainty of the technology’s future. Will agents become more autonomous? Will multimodality become standard? Will fine-tuning give way to new adaptation techniques?</p><p>Think of your architecture like building a house with an expandable floor plan. Rather than trying to predict exactly how many children you might have someday, you create a design where adding rooms is straightforward. In technical terms, this means focusing on systems with clear extension points and abstraction layers.</p><p>This flexible foundation approach has proven successful across many AI frameworks. We’ve seen orchestration systems evolve from simple sequential workflows to sophisticated graph-based executions without requiring developers to throw away their existing code. It’s like how your smartphone can gain new capabilities through software updates without requiring you to buy an entirely new device.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7PiaslYERfJeFSHDQj-y2w.png" /></figure><h3>Conclusion: The Architectural Mindset</h3><p>As technological leaders, our job isn’t just to implement today’s solutions but to build foundations for tomorrow’s possibilities. With LLMs, this requires a particular mindset — one that values flexibility over certainty, observability over assumption, and evolution over perfection.</p><p>By embracing modular design, prioritizing observability, planning for adaptation, and implementing thoughtful governance, we create AI systems that don’t just work today but continue to deliver value as the technology landscape evolves.</p><p>The choices we make now in our LLM architecture will either empower our organizations to ride the wave of AI innovation or leave them struggling to keep up with each new development. By building on solid architectural ground, we ensure it’s the former rather than the latter.</p><p>Are your AI systems built to last, or just built to launch? The architecture you choose today will determine the answer tomorrow.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b54cb80ef338" width="1" height="1" alt=""><hr><p><a href="https://medium.com/similarweb-engineering/building-on-solid-ground-the-critical-importance-of-good-architecture-for-llm-tools-b54cb80ef338">Building on Solid Ground: The Critical Importance of Good Architecture for LLM Tools</a> was originally published in <a href="https://medium.com/similarweb-engineering">Similarweb Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Beyond Code: How Tech Leaders Can Drive Organizational Growth Through Knowledge Sharing]]></title>
            <link>https://medium.com/similarweb-engineering/beyond-code-how-tech-leaders-can-drive-organizational-growth-through-knowledge-sharing-b9f57096ecac?source=rss-b56441e272bc------2</link>
            <guid isPermaLink="false">https://medium.com/p/b9f57096ecac</guid>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[engineering-management]]></category>
            <category><![CDATA[leadership]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[software-engineering]]></category>
            <dc:creator><![CDATA[Dor Amram]]></dc:creator>
            <pubDate>Sun, 17 Nov 2024 07:11:47 GMT</pubDate>
            <atom:updated>2024-11-17T07:11:47.595Z</atom:updated>
            <content:encoded><![CDATA[<p>As technology professionals, we often measure our impact through the code we write, the systems we design, and the technical problems we solve. However, as we progress in our careers, particularly into leadership roles, we discover that our ability to influence organizational success extends far beyond our technical contributions. In this article, I’ll share my personal journey of identifying and addressing an organizational need through educational initiative, and explore how tech leaders can create lasting impact through non-coding activities.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/912/1*HzDpacfVK0bW2Pyv4b4hlw.png" /></figure><h3>The Evolution of Technical Leadership</h3><p>When I first stepped into the tech lead role, my focus was primarily on system design and helping with complex technical challenges. These responsibilities aligned with the traditional expectations of technical leadership — being the go-to person for difficult engineering problems and architectural decisions. However, a recent experience taught me that some of our most significant contributions might come from unexpected directions.</p><p>The conventional path of technical leadership often follows a predictable trajectory: start as a strong individual contributor, take on more complex technical challenges, and gradually assume responsibility for technical decisions affecting entire systems or teams. This progression makes sense, as it builds upon our core strengths and established expertise. However, this linear path can sometimes blind us to other equally important opportunities for impact.</p><h3>The Changing Landscape of Technical Leadership</h3><p>Today’s technology organizations face challenges that extend beyond pure technical excellence:</p><ul><li>Rapidly evolving technology stacks requiring continuous learning</li><li>Growing teams with diverse skill levels and backgrounds</li><li>Increased emphasis on soft skills and cross-functional collaboration</li><li>Need for scalable knowledge sharing and standardization</li><li>Balance between innovation and maintainable solutions</li></ul><p>These challenges require technical leaders to expand their toolkit beyond coding and system design, embracing roles as educators, mentors, and cultural ambassadors.</p><h3>Identifying the Opportunity</h3><p>It began with a small-scale course I had developed to enhance our analysis team’s engineering capabilities. The positive impact of this initiative caught the attention of our CTO, who recognized the potential for a broader application. This led to a request to develop a comprehensive training program for our new graduate hires — young engineers fresh out of college whom the company was eager to invest in.</p><p>The request aligned with a pattern I had observed: while our junior engineers were technically skilled, they needed guidance in applying their knowledge within our specific business context and development practices. This gap between academic knowledge and practical application represented both a challenge and an opportunity.</p><h3>The Analysis That Led to Action</h3><p>The path to expanding our training program began with observing recurring patterns in our development process. Through code reviews and technical discussions, I noticed consistent knowledge gaps among our junior engineers. While they possessed strong theoretical foundations from their academic backgrounds, they often struggled to apply this knowledge effectively in our production environment. Common issues surfaced repeatedly: misconceptions about how our systems operated at scale, inconsistent approaches to coding practices, and difficulties in translating their theoretical knowledge into practical solutions. These patterns weren’t just isolated incidents — they represented a systematic gap between academic learning and real-world application.</p><p>The impact of these knowledge gaps extended beyond individual performance issues. Our teams were experiencing varying onboarding experiences, leading to inconsistent skill development across the organization. Some teams had developed their own informal training approaches, while others relied heavily on one-on-one mentoring. This inconsistency meant that new hires’ time-to-productivity varied significantly depending on their team placement. Furthermore, knowledge silos were beginning to form within teams, making it difficult to maintain consistent practices across the organization and limiting opportunities for cross-team collaboration.</p><p>Looking at these observations through a business lens revealed the broader implications of these challenges. Projects were taking longer than necessary to complete as senior engineers spent significant time reviewing and correcting common mistakes. The inconsistent practices across teams were contributing to technical debt, which would eventually require dedicated time and resources to address. More importantly, our review cycles were becoming increasingly lengthy, affecting our overall team velocity. These business impacts made it clear that a more structured approach to knowledge sharing wasn’t just desirable — it was essential for our organization’s continued growth and efficiency.</p><h3>From Recognition to Action</h3><p>Taking on this educational initiative was a departure from my usual technical leadership responsibilities. However, the decision to embrace this challenge was driven by several key considerations:</p><ol><li><strong>Scalable Impact</strong>: While solving technical problems has immediate value, creating educational content has the potential to impact multiple generations of engineers.</li><li><strong>Knowledge Standardization</strong>: A structured course ensures consistent knowledge transfer across teams and reduces the repetitive nature of one-on-one mentoring.</li><li><strong>Cultural Investment</strong>: Demonstrating the organization’s commitment to employee development helps build a culture of continuous learning and growth.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/912/1*gB5ojd3he6wQkJFv0jhsIQ.png" /></figure><h3>The Decision-Making Process</h3><p>The journey from identifying the educational need to committing to a comprehensive solution wasn’t straightforward. My first step was conducting a thorough assessment of our available resources — evaluating the time investment required, gauging potential support from team members, and identifying the technical resources we’d need to create effective training materials. I also had to carefully consider how this initiative would impact my ongoing technical responsibilities and existing workflows. The prospect of disrupting our team’s rhythm with this new undertaking weighed heavily on my mind, as did the learning curve I’d face in creating educational content.</p><p>This careful deliberation led me to weigh the opportunity costs against potential benefits. While taking on this educational initiative meant temporarily stepping back from some technical projects, the long-term impact on team development appeared far more valuable. The possibility of creating lasting change through knowledge sharing, combined with my own growth opportunities in content creation and mentorship, ultimately tipped the scales. The decision to proceed wasn’t just about immediate team needs — it was an investment in our organization’s future, promising returns through improved code quality, faster onboarding, and a stronger engineering culture.</p><h3>The Implementation Journey</h3><p>Developing the course required a different skill set from my usual technical work. Here are some key lessons learned:</p><h3>Content Architecture</h3><p>Just as we architect software systems, educational content needs careful structuring:</p><ul><li>Breaking down complex concepts into digestible modules</li><li>Creating a logical progression of topics</li><li>Balancing theoretical knowledge with practical exercises</li><li>Incorporating real-world examples</li></ul><h4>Curriculum Design Principles</h4><p>The course structure was built on the principle of progressive complexity, carefully guiding participants from foundational concepts to advanced topics. We designed the learning journey to mirror the natural growth of a software engineer, starting with basic principles and gradually introducing more complex challenges. Regular checkpoint exercises were strategically placed throughout the curriculum, allowing participants to validate their understanding before moving forward. This stepped approach proved particularly effective as it gave new engineers the confidence to tackle increasingly sophisticated problems while maintaining a clear connection between each new concept and their existing knowledge.</p><p>The curriculum’s strength lay in its deeply practical focus and interactive nature. Rather than relying on theoretical exercises, we incorporated real-world examples, making the learning immediately applicable to daily work. Hands-on sessions became the cornerstone of our approach, featuring group discussions, and intensive workshops. System design exercises were structured as collaborative sessions where participants could explore architectural decisions together, learning not just from the instructor but from each other’s perspectives and experiences. This combination of practical application and interactive learning created an engaging environment where engineers could safely experiment with new concepts while building the confidence to apply them in their actual work.</p><h3>Measuring Success</h3><p>Throughout the course delivery, I took an active role in monitoring and measuring its effectiveness, personally conducting code reviews for participants to track their progress firsthand. This direct involvement gave me invaluable insights into how well the concepts were being absorbed and applied in practice. After each section of the course, I distributed detailed surveys to gather immediate feedback from participants, helping us understand which topics resonated most strongly and which areas needed reinforcement. The responses from these surveys proved instrumental in fine-tuning the content and delivery methods as we progressed, ensuring the course remained both engaging and effective.</p><p>To ensure a comprehensive view of the program’s impact, I maintained regular communication with the participants’ team leaders throughout the training period. Their feedback was consistently positive, with team leaders reporting that their new engineers were demonstrating a solid grasp of the material and showing genuine enthusiasm for the course content. While the participants hadn’t yet been granted production access, their performance in training exercises and practice scenarios showed promising signs of their readiness for real-world challenges. The team leaders’ observations, combined with the participants’ own positive feedback through surveys, validated our approach to structured learning and confirmed that we were effectively preparing these engineers for their future responsibilities. This early success suggested that our educational initiative was laying a strong foundation for these new engineers to become valuable contributors once they began working with production systems.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/912/1*s84qyyRxYdaoCp3DTG6Mog.png" /></figure><h3>The Broader Impact</h3><p>The impact of this educational initiative began revealing itself even during the course delivery, showing promise beyond its initial scope as a training program for new graduates. The process of creating and delivering structured educational content forced us to examine and articulate our engineering practices in unprecedented detail. As we developed the curriculum, we found ourselves documenting processes that had previously existed only as tribal knowledge, creating clear guidelines and explanations that could benefit not just course participants but potentially the entire engineering organization.</p><p>The course development process also sparked valuable discussions among our experienced engineers who contributed to the content. These conversations about best practices, common pitfalls, and effective teaching approaches led to thoughtful examinations of our own processes. We found ourselves questioning and refining our approaches to code review, development workflows, and quality assurance practices. The simple act of preparing to teach others prompted us to clarify and sometimes improve our own methods, creating a more structured and well-documented approach to our engineering practices.</p><p>The collaborative atmosphere that emerged during the course delivery was particularly encouraging. As we worked through the curriculum with participants, we observed an increasing willingness to ask questions and engage in technical discussions. The shared learning environment created a space where it was safe to explore ideas and learn from mistakes, fostering the kind of supportive culture we hoped to build in our engineering teams. While it’s too early to assess the long-term impact of this initiative, these early signs suggest that our investment in structured learning and knowledge sharing has the potential to strengthen our engineering practice in meaningful ways.</p><h3>Expanding Your Impact Toolkit</h3><p>For tech leads looking to expand their influence beyond code, consider these approaches:</p><h3>1. Identify Knowledge Gaps</h3><p>For tech leads looking to create a similar impact in their organizations, the journey should begin with a careful assessment of knowledge gaps within their teams. In my experience, this process started with paying closer attention to the daily challenges faced by team members, particularly noting recurring questions and common stumbling blocks. Through observing code review discussions, architecture planning sessions, and day-to-day problem-solving activities, patterns began to emerge. These patterns highlighted not just technical skill gaps, but also areas where theoretical knowledge from academic backgrounds wasn’t translating effectively into practical application within our specific business context.</p><p>The identification process shouldn’t stop at technical skills alone. By engaging in conversations with team members across different experience levels and roles, I discovered gaps in understanding of our broader business context and development practices. These discussions revealed opportunities for improvement in areas such as system design approaches, performance optimization strategies, and cross-team collaboration practices. Understanding these gaps required looking beyond immediate technical challenges to see how our teams’ capabilities aligned with the organization’s strategic objectives and growth plans. This broader perspective helped ensure that any educational initiative we developed would address not just immediate technical needs, but also support the long-term success of both the team and the organization.</p><h3>2. Start Small</h3><p>While the desire to create comprehensive educational programs might be strong, I’ve learned that starting with smaller, focused initiatives can provide valuable insights and build momentum. My journey began with a single focused workshop addressing a specific technical challenge our team was facing. This modest start allowed me to experiment with different teaching approaches and content delivery methods without the pressure of a full-scale program. The immediate feedback and lessons learned from this initial effort were invaluable in understanding what resonated with participants and what needed adjustment, providing a solid foundation for future expansion.</p><p>The key to successfully growing these educational initiatives lies in maintaining consistent feedback loops and being willing to iterate. After each workshop or training session, I made it a point to gather detailed feedback from participants and incorporate their suggestions into the next iteration. This incremental approach not only helped refine the content and delivery but also built credibility within the organization. As team members began to see the value in these focused learning sessions, support for larger initiatives naturally grew. By demonstrating success on a smaller scale first, we were able to build the trust and organizational buy-in necessary to eventually develop more comprehensive training programs.</p><h3>3. Leverage Technical Expertise</h3><p>Your technical expertise as a senior engineer is your greatest asset when creating educational content, but I discovered that the key lies in how you translate this knowledge into digestible learning materials. Throughout the development of our course, I found myself constantly drawing from my own experiences with production systems and architectural decisions. Rather than presenting theoretical concepts in isolation, I focused on connecting them to real scenarios we’d encountered in our work. When explaining a particular design pattern or best practice, I would accompany it with examples of how it solved actual problems we’d faced, or in some cases, how its absence had led to challenges in past projects.</p><p>The art of leveraging technical expertise for education goes beyond just sharing what you know — it’s about creating meaningful learning experiences that bridge the gap between theory and practice. I structured hands-on exercises around simplified versions of real challenges we’d encountered, allowing participants to experience the same problem-solving process we go through in our daily work. When discussing system architecture, I would walk through the evolution of our own systems, explaining the decisions we made and their consequences. This approach of grounding everything in practical experience helped participants understand not just the how of our technical practices, but also the crucial why behind our decisions, making the learning more engaging and immediately applicable to their work.</p><h3>4. Build Support Networks</h3><p>The success of any educational initiative relies heavily on building a strong network of support throughout the organization. In our case, this began with engaging our technical leadership team, sharing the vision for the training program and demonstrating how it aligned with our organization’s growth objectives. These early conversations proved crucial in securing not just approval, but active support for the initiative. Senior engineers across different teams began expressing interest in contributing their expertise, offering to review content or share their experiences in specific technical areas. This collaborative approach helped ensure the program’s content was well-rounded and reflected the diverse technical perspectives within our organization.</p><p>Equally important was fostering connections with the broader engineering community within our company. Regular updates about the program’s development and its potential impact helped maintain visibility and generate interest. I found that keeping communication channels open with team leaders and potential participants helped shape the program to better meet their needs. This ongoing dialogue created a sense of shared ownership in the program’s success, with various team members volunteering their time and insights to improve the content. The support network we built wasn’t just about securing resources — it became a community of people invested in the growth and development of our engineering talent.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/912/1*hak9clLqLKBizTSyndRX1A.png" /></figure><h3>Conclusion</h3><p>As technical leaders, our value to the organization extends far beyond our ability to write code or design systems. By identifying opportunities to share knowledge and build capabilities within our teams, we can create lasting impact that scales beyond our direct technical contributions.</p><p>The journey from developing a small course to creating a comprehensive training program taught me that some of our most significant contributions might come from stepping outside our comfort zones. Whether through educational initiatives, process improvements, or cultural engineering, tech leads have unique opportunities to shape their organizations in meaningful ways.</p><p>Remember, every piece of knowledge shared, every process improved, and every junior engineer mentored contributes to the organization’s long-term success. As you progress in your technical leadership role, consider how you might expand your impact beyond code. The future of technical leadership lies not just in what we can build ourselves, but in how we can empower and elevate entire teams to build better solutions together.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b9f57096ecac" width="1" height="1" alt=""><hr><p><a href="https://medium.com/similarweb-engineering/beyond-code-how-tech-leaders-can-drive-organizational-growth-through-knowledge-sharing-b9f57096ecac">Beyond Code: How Tech Leaders Can Drive Organizational Growth Through Knowledge Sharing</a> was originally published in <a href="https://medium.com/similarweb-engineering">Similarweb Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Builders Forum: How We Started a Thriving Technical Guild]]></title>
            <link>https://medium.com/similarweb-engineering/the-builders-forum-how-we-started-a-thriving-technical-guild-62917ff5e438?source=rss-b56441e272bc------2</link>
            <guid isPermaLink="false">https://medium.com/p/62917ff5e438</guid>
            <category><![CDATA[community]]></category>
            <category><![CDATA[diversity]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[guild]]></category>
            <category><![CDATA[best-practices]]></category>
            <dc:creator><![CDATA[Dor Amram]]></dc:creator>
            <pubDate>Mon, 09 Sep 2024 14:13:29 GMT</pubDate>
            <atom:updated>2024-09-09T14:13:29.679Z</atom:updated>
            <content:encoded><![CDATA[<p>Last year, a friend and I were lamenting how siloed the backend engineering teams at our company had become. Each team was heads down working on their own microservices and projects, but there was little cross-team collaboration or knowledge sharing happening. We all faced similar technical challenges, but rarely got to learn from each other.</p><p>That’s when the idea hit us — what if we started a backend engineering guild to bring people together? A forum for “builders” across teams to connect, share ideas, and learn. We weren’t sure exactly what it would look like, but we knew we had to give it a shot.</p><p>Here’s the story of how we started “The Builders Forum”, the backend engineering guild at our company, and some tips for starting your own thriving technical guild.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/720/1*f_ARn-PaqoBfYcM-PmgiYg.jpeg" /></figure><h3>Gauging Interest</h3><p>First, we wanted to see if others were interested in the idea of a backend guild. We put out some feelers by having casual conversations with engineers on various teams. Whenever the topic of cross-team collaboration or knowledge sharing came up, we’d mention the idea of starting a guild. To our delight, many were enthusiastic about it. They faced the same isolation we did and craved more connection with their fellow backend devs.</p><p>These informal conversations confirmed there was significant interest and gave us the confidence to move forward. We also got a lot of great ideas from these chats that helped shape the direction of the guild.</p><h3>Kick-off Meeting</h3><p>We scheduled an initial kick-off meeting and invited anyone who was interested. The purpose was to introduce the concept of the guild and gauge people’s interest in participating and contributing.</p><p>To start off, we presented the idea ourselves. We wanted to set the tone that this was a collaborative effort and that everyone’s participation was welcome and valued. By presenting first, we hoped to make others feel more comfortable with the idea of presenting and sharing at future meetings.</p><p>The turnout was great and the response was very positive. Many attendees expressed excitement about the guild and volunteered to give talks or lead sessions on various backend topics they were passionate about.</p><p>We also brainstormed between ourselves about different types of events and activities the guild could host: — Talks and presentations on backend topics — Roundtable discussions — Code reviews and collaboration sessions — Workshops and tutorials — Hackathons — Social events and outings</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/720/1*ZoKO12vg1a8NJCscR-hndA.jpeg" /></figure><h3>Hosting Regular Events</h3><p>With the positive response from the kick-off meeting, we started hosting regular guild events. We aimed to have at least one event per month, with a variety of formats.</p><p>Some of our most popular events have included:</p><p>- A talk on FastAPI best practices</p><p>- A comparison between serverless and monolith architecture within our organization</p><p>- A new approach for using Airflow operators</p><p>- A deep dive for our yearly Hackathon winning project</p><p>For each event, my partner and I take on the organization tasks. We create an agenda, coordinate with speakers or facilitators, book a room, order food, and invite the whole backend engineering org. It’s a lot of work, but we’re committed to making the guild a success.</p><p>We also set up a dedicated Slack channel for the guild where we announce upcoming events, share relevant articles and resources, and keep the discussion going between meetings. This keeps engagement high and makes the guild feel like an active community.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pgdAEjh8HtmnGXbWXb70GQ.png" /></figure><h3>Lessons Learned</h3><p>It’s been a year since we started The Builders Forum, and while it’s been very successful, we’ve also learned some lessons along the way:</p><p>1. Consistency is key. It’s easy to let the guild slide when things get busy, but having a consistent schedule of events that people can plan around keeps momentum and engagement high.</p><p>2. Embrace variety. Different people like different formats, so having a mix of talks, discussions, social events, etc. helps keep things fresh and appeals to a range of preferences.</p><p>3. Encourage participation. Encouraging others to lead sessions and share their knowledge has been crucial. It’s not just about my partner and I presenting.</p><p>4. Get leadership buy-in. Our engineering managers and CTO have been supportive of the guild, which helps a lot with things like booking rooms and justifying time spent on it. Make sure your leadership sees the value.</p><p>5. Keep it fun. At the end of the day, the guild is an opportunity to connect with fellow backend devs and geek out together. The more fun it is, the more people want to engage. Don’t take it too seriously and optimize for enjoyment.</p><h3>Get Started!</h3><p>Starting The Builders Forum has been incredibly rewarding. It’s broken down silos, sparked collaborations, leveled up skills across the board, and formed a thriving backend community at our company. If your engineering org could benefit from more connection and collaboration (and whose couldn’t?), I highly recommend starting your own technical guild.</p><p>Use this post as a rough guide, but make it your own! Talk to engineers on different teams to gauge interest and gather ideas. Find a co-conspirator to help with the organizing. Schedule an initial kick-off meeting to introduce the idea and see who’s interested in participating and contributing. Then pick a date for your first event and make it happen!</p><p>With some passion and consistency, you’ll have a thriving technical guild of your own in no time. Trust me, your engineering org will thank you. Now get out there and start connecting and collaborating!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=62917ff5e438" width="1" height="1" alt=""><hr><p><a href="https://medium.com/similarweb-engineering/the-builders-forum-how-we-started-a-thriving-technical-guild-62917ff5e438">The Builders Forum: How We Started a Thriving Technical Guild</a> was originally published in <a href="https://medium.com/similarweb-engineering">Similarweb Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Resilience in Code: Lessons Learned After 97 Days at War]]></title>
            <link>https://medium.com/similarweb-engineering/resilience-in-code-lessons-learned-after-97-days-at-war-bdb04cdca5d9?source=rss-b56441e272bc------2</link>
            <guid isPermaLink="false">https://medium.com/p/bdb04cdca5d9</guid>
            <category><![CDATA[resilience]]></category>
            <category><![CDATA[tech]]></category>
            <category><![CDATA[culture]]></category>
            <dc:creator><![CDATA[Dor Amram]]></dc:creator>
            <pubDate>Mon, 22 Jan 2024 16:15:21 GMT</pubDate>
            <atom:updated>2024-01-23T11:09:10.919Z</atom:updated>
            <content:encoded><![CDATA[<p><strong>Introduction</strong></p><p>Three months ago, life as I knew it was upended. The onset of war in my country led to a swift and unexpected transition for many of us, including myself, from my role as software engineer to fulfilling civic duty in the reserves. This abrupt change left a profound impact, not just on me but on several of my colleagues who found themselves in similar situations, with some still serving. This blog post is a reflection on this shared experience, focusing on the unique challenges and learnings that come with such sudden transitions in my professional life.</p><p><strong>Understanding the Process</strong></p><p>The experience of leaving a familiar work environment for an uncertain future brought about a profound realization of the challenges faced in such transitions. The first lesson was clear: the importance of robust, adaptable processes in a tech team. When a key member steps away suddenly, the team’s ability to maintain momentum hinges on these established practices. In my case, and for many of my colleagues, the disruption highlighted the crucial role of clear communication channels and flexible workflows, which had been set up but not fully appreciated until tested by these circumstances.</p><p>The second lesson was about the resilience of our team dynamics. The absence of one or more team members can strain a team, yet it can also reveal the strength of the collective. It’s in these moments that the team’s adaptability and cohesiveness are truly tested. For us, it meant redistributing responsibilities, relying more on remote collaboration tools, and finding new ways to support each other, both professionally and emotionally.</p><p><strong>The Essence of Good Coding Practices During Absences</strong></p><p>In the whirlwind of our sudden departures, the underlying principles of good coding practices stood out as a beacon. These practices, often summed up as ‘clean code’, go beyond mere neatness. They encompass writing code that is not just functional but also clear, well-organized, and easily maintainable. In our absence, this approach to coding proved to be a critical asset.</p><p>The true power of these practices became apparent when we returned and seamlessly re-engaged with our projects. The code we revisited wasn’t a puzzle to be solved; it was a clear map, guiding us through the logic and decisions made in our absence. This clarity in code meant less time deciphering and more time contributing effectively, facilitating a smoother transition back into the team.</p><p>Moreover, these coding principles fostered a sense of ongoing collaboration. Despite not being physically present, the well-structured code acted as a continuous thread of communication. It bridged the gaps left by our absence, ensuring that projects didn’t just survive but thrived. This experience underscored the fact that good coding practices are more than technical necessities; they are vital for sustaining team momentum and adaptability in times of change</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*YLJ9Q7nlsALFkQwf" /></figure><p><strong>Personal Insights on Team Communication</strong></p><p>During my time away and upon my return, the critical role of efficient communication within the team became increasingly apparent. It wasn’t just about staying in touch; it was about ensuring that the communication was effective and facilitated a smooth transition.</p><p>The essence of good communication in our team was rooted in clarity and precision. Every interaction, whether it was a quick update or a detailed discussion, was approached with a focus on clear, concise, and meaningful exchanges. This approach reduced misunderstandings and streamlined our collaborative efforts, making it easier for me and others who were away to reintegrate seamlessly.</p><p>Additionally, the importance of working cleanly and methodically played a significant role in our communication efficiency. When code, documentation, and project plans are structured and well-organized, they communicate as effectively as a well-crafted email or meeting. This clarity in our work products meant that a significant portion of our communication was already embedded in the work itself, reducing the need for lengthy explanations and clarifications.</p><p>This combination of efficient communication and clean working practices created an environment where information flowed smoothly and collaboration was effortless. It highlighted that effective communication is not just about the frequency or methods used but is deeply interconnected with how we approach our work. The efforts made by my team in maintaining these standards significantly eased the challenges of my reintegration, making it a cohesive and productive experience.</p><p>This experience taught a crucial lesson: the symbiotic relationship between effective communication and orderly work practices is key to team resilience and adaptability, particularly during times of transition.</p><p><strong>Adapting to Task Nature During Transition Periods</strong></p><p>There was a brief period during my absence when I was able to return to the team for a few days. This short stint back in the office offered a unique insight into how task assignments adapt during such transition periods. Instead of diving back into the deep end with mission-critical tasks, I was assigned to handle smaller, infrastructure-related tasks. This strategic choice by our team leadership proved to be incredibly beneficial for several reasons.</p><p>Firstly, these tasks, while less critical, were vital for the smooth running of our systems. They included minor coding tasks, system updates, and small-scale project management. This focus allowed me to contribute meaningfully without the pressure of immediately tackling complex, high-stakes projects.</p><p>Secondly, handling these tasks provided me with the opportunity to reacquaint myself with the team’s current workflow and technological stack. Changes, however minor, had occurred during my absence, and these tasks acted as a gentle reintroduction to these new elements.</p><p>Lastly, this approach had a positive impact on team dynamics. It eased the burden of my readjustment on my colleagues, ensuring that their workflow wasn’t disrupted by my need to catch up. It also demonstrated the team’s understanding and support, acknowledging that a gradual reintegration was more effective than an immediate deep dive.</p><p>This experience underscored a valuable lesson: the importance of thoughtful task allocation during transitional periods. It not only aids in the smooth reintegration of returning team members but also maintains team stability and productivity. It’s a strategy that not only benefits the returning team member but also the entire team, fostering a more supportive and adaptable work environment.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*jbGnCngEC2YiAx5z" /></figure><p><strong>Transitioning Back to Civilian Life</strong></p><p>Returning to civilian life after serving in the reserves is a process, one that unfolds over time and requires patience and understanding. This transition is not just a change in routine; it’s a shift in mindset and environment. It’s essential to recognize and accept that it’s okay for this adjustment to take time.</p><p>My own journey back to the civil world was significantly eased by the unwavering support I received from my manager. Their understanding and empathy during this period were invaluable. They recognized the challenges of this transition and provided the necessary space and support for me to gradually readjust to the civilian work environment. This support was not just about easing back into work; it was about reorienting myself to a life that had momentarily taken a backseat.</p><p>The patience and guidance from my manager and colleagues reminded me that it’s not about rushing to ‘get back to normal’ but allowing oneself the time and space to adapt at a comfortable pace. Their support was a critical element in my smooth transition, reinforcing the idea that the journey back to civilian life is a gradual process, one that benefits greatly from a supportive and understanding work environment.</p><p><strong>Conclusion</strong></p><p>The journey of stepping away and then returning to a tech team, especially under extraordinary circumstances like a national crisis, is fraught with challenges. Yet, it also brings invaluable lessons and opportunities for growth. The experiences shared in this blog post — from adapting to changes in team dynamics to embracing efficient communication and orderly work practices — demonstrate that we are equipped with the tools and resilience needed to navigate whatever lies ahead.</p><p>As I reflect on the myriad of challenges and transitions we’ve faced, one thing becomes clear: the future is uncertain, but our preparation for it is not. While we cannot predict the future, our journey thus far has armed us with adaptability, empathy, and innovative thinking. These are the tools that will guide us through unknown territories. It’s with this mindset that we can approach the future, not with apprehension, but with the confidence that comes from knowing we are prepared to face and adapt to whatever challenges it may bring.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*UR5jCldsdTgT336G" /></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=bdb04cdca5d9" width="1" height="1" alt=""><hr><p><a href="https://medium.com/similarweb-engineering/resilience-in-code-lessons-learned-after-97-days-at-war-bdb04cdca5d9">Resilience in Code: Lessons Learned After 97 Days at War</a> was originally published in <a href="https://medium.com/similarweb-engineering">Similarweb Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Navigating Rough Waters: Shedding Technical Debt]]></title>
            <link>https://medium.com/similarweb-engineering/navigating-rough-waters-shedding-technical-debt-66130402b375?source=rss-b56441e272bc------2</link>
            <guid isPermaLink="false">https://medium.com/p/66130402b375</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[best-practices]]></category>
            <category><![CDATA[code-quality]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[clean-code]]></category>
            <dc:creator><![CDATA[Dor Amram]]></dc:creator>
            <pubDate>Mon, 04 Sep 2023 10:36:14 GMT</pubDate>
            <atom:updated>2023-09-04T10:47:16.006Z</atom:updated>
            <content:encoded><![CDATA[<p>If you’ve been in the software engineering field for even a short period, you’ve likely encountered the beast we all know as “technical debt.” I’ve been there, too — staring at a screen filled with spaghetti code, wondering how we got here. Over the years, I’ve learned that technical debt isn’t just an annoying byproduct of development; it’s a reality that, if not managed well, can cripple even the most promising projects. In this blog post, I want to share my personal experiences and the strategies I’ve found effective for fighting technical debt. I’ll also talk about how I’ve been working on creating what I like to call a “technical immune system” to keep this debt in check.</p><h4>The Reality of Technical Debt</h4><p>Technical debt is like that credit card bill you keep saying you’ll pay off next month but never do. It accumulates interest, and before you know it, you’re stuck in a cycle of just paying the minimum amount, never really reducing the principal. In software terms, this means your team is spending more time fixing bugs and navigating through complex, outdated code than actually building new features.</p><h4>Strategies for Fighting Tech Debt: A Deeper Dive</h4><p><strong>Regular Audits: The Health Check-Ups</strong></p><p>Regular audits are akin to the health check-ups we all know we should be getting but often neglect. In the context of software engineering, these audits serve as a diagnostic tool to identify areas of the codebase that have accumulated technical debt. I’ve found that setting aside time for these audits at least once a quarter has been invaluable.</p><p>But the audit doesn’t stop at identification; it extends to action. Once the audit is complete, we categorize the issues based on their severity and impact. We then create actionable tickets and prioritize them in our development backlog. This ensures that the identified issues don’t just sit there but are actively addressed in subsequent sprints.</p><p>The key to making audits effective is consistency and follow-through. It’s easy to conduct an audit once and forget about it, but the real value comes from making it a recurring activity. This allows us to track our progress over time and ensures that we’re moving in the right direction in terms of code quality and maintainability.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/966/1*uoiQfo3rp84Pl4_0IHiw1Q.png" /></figure><p><strong>Prioritize Refactoring: The Diet Plan</strong></p><p>Refactoring is the “diet plan” of the software world. We all know it’s good for us, but it’s often the first thing to be sacrificed when deadlines loom. I’ve been guilty of this more times than I’d like to admit. However, I’ve come to realize that consistent, small-scale refactoring is far more manageable and effective than occasional, large-scale overhauls.</p><p>To make refactoring a priority, I’ve started allocating a fixed percentage of each quarter solely for these tasks. This ensures that refactoring becomes an integral part of our development cycle rather than a one-off activity that happens “when we have time.” The trick is to make it non-negotiable, just like you would with a diet plan.</p><p>The benefits of this approach are twofold. First, it helps in gradually reducing the existing technical debt. Second, it prevents the accumulation of new debt by ensuring that we continuously improve the codebase. It’s a proactive approach that pays dividends in the long run.</p><p><strong>Automated Testing: The Exercise Regimen</strong></p><p>Automated testing is the exercise regimen that keeps your codebase fit and healthy. I’ve found that a robust automated testing framework is an invaluable asset in the fight against technical debt. We use a combination of unit tests, integration tests, and end-to-end tests to cover as much ground as possible. These tests are run automatically as part of our CI/CD pipeline, ensuring that any new code or changes to existing code are thoroughly vetted before being deployed.</p><p>The beauty of automated testing is that it provides immediate feedback. If a piece of code doesn’t meet the expected standards or if it breaks existing functionality, we know right away. This allows us to catch issues early in the development cycle, making them easier and less costly to fix.</p><p>Moreover, a strong testing framework acts as a safety net, giving developers the confidence to refactor and make changes to the codebase. This is crucial for reducing technical debt, as it allows us to improve the code quality without the fear of breaking existing functionality.</p><p><strong>Code Reviews: The Personal Trainer</strong></p><p>Code reviews are the personal trainers of the software development world. They provide an external perspective, catch potential issues, and push you to do better. I’ve made it a rule in my team that no code gets merged into the main branch without undergoing a peer review. This practice serves multiple purposes.</p><p>First, it acts as a quality gate, ensuring that the code meets the team’s standards both in terms of functionality and readability. Second, it fosters a culture of collective code ownership. When multiple eyes scrutinize every line of code, it’s less likely that technical debt will slip through the cracks.</p><p>Lastly, code reviews are an excellent platform for knowledge sharing and mentorship. More experienced team members can provide insights and best practices, while less experienced members get the opportunity to learn and improve. It’s a win-win situation that not only helps in reducing technical debt but also contributes to the team’s overall growth and development.</p><h4>Building a Technical Immune System</h4><p><strong>Monitoring: The Vital Signs</strong></p><p>Monitoring is the heartbeat of a technical immune system. Imagine walking into a hospital room where the patient’s vital signs are continuously displayed on a monitor. The doctors and nurses can instantly see if something goes wrong. Similarly, in the realm of software engineering, monitoring tools act as our eyes and ears, continuously scanning the codebase for “vital signs” like code complexity, dependency vulnerabilities, and performance metrics.</p><p>Monitoring is not just about collecting data; it’s about making sense of it. We use tools like DataDog to visualize this data in real-time dashboards. We set up alerts that notify us via Slack or email if certain thresholds are crossed. For instance, if the build fails or if some data is missing, the team is immediately alerted.</p><p>The beauty of monitoring is that it allows for proactive rather than reactive measures. Instead of waiting for a system to fail or for a user to report an issue, we can identify potential problems before they escalate. This is akin to catching a disease in its early stages, making it much easier to treat. Monitoring is the cornerstone of our technical immune system, providing the data we need to make informed decisions.</p><p><strong>Automated Workflows: The Auto-Healing Mechanism</strong></p><p>Automated workflows are the auto-healing mechanisms of our technical immune system. Just like white blood cells in our body rush to the site of an infection to combat pathogens, automated workflows kick in when they detect issues in the codebase. We use Continuous Integration/Continuous Deployment (CI/CD) pipelines to automate a series of checks and balances. These pipelines run a battery of tests, perform code quality assessments, and even auto-refactor code where possible.</p><p>The power of automation lies in its consistency and speed. Manual processes are prone to human error and can be time-consuming. Automated workflows, on the other hand, execute the same set of tasks with machine-like precision, and they do it fast. This ensures that any new code or changes to existing code meet the predefined quality standards before they are deployed.</p><p>Moreover, these workflows are not set in stone; they evolve. As we identify new types of issues or adopt new technologies, we update our workflows to include checks for them. This adaptability makes our technical immune system resilient and up-to-date, capable of dealing with new “pathogens” as they emerge.</p><p><strong>Knowledge Sharing: The Collective Immunity</strong></p><p>Knowledge sharing is what I like to call the “collective immunity” of our technical ecosystem. Just as herd immunity protects a community from the spread of diseases, a shared knowledge base safeguards the team from repeating past mistakes and poor practices. We maintain an internal wiki that serves as a repository for all things technical — best practices, coding guidelines, lessons learned from past incidents, and even architectural decisions.</p><p>This knowledge base is a living document, continuously updated and enriched by contributions from team members. New hires are encouraged to go through this repository as part of their onboarding process. This not only brings them up to speed but also instills a culture of knowledge sharing right from the start.</p><p>The impact of this collective knowledge is exponential. It not only helps in reducing the introduction of new technical debt but also aids in faster problem-solving. When faced with a challenge, team members can refer to the knowledge base to see if a similar issue has been tackled before, saving time and effort.</p><p><strong>Feedback Loops: The Body’s Response System</strong></p><p>Feedback loops are akin to the body’s nervous system, sending signals from various parts to the brain for interpretation and action. In our technical environment, these loops are channels of continuous feedback from all stakeholders — developers, QA teams, product managers, and even end-users. We use tools like Jira and Slack to facilitate this communication, and we hold regular retrospectives to discuss what’s working and what’s not.</p><p>Feedback loops serve multiple purposes. First, they help us gauge the impact of technical debt on the product and user experience. Second, they provide insights into areas that may not be on our radar but are causing pain points for others. For example, a feature that we consider “done” might be causing usability issues for the end-users.</p><p>By closing the feedback loop, we not only improve the product but also fine-tune our technical immune system. We learn from our mistakes and successes alike, making necessary adjustments to our strategies, tools, and workflows. This iterative learning process is what makes our technical immune system robust and effective, capable of adapting to new challenges and complexities.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*t8ivIcim8VzrG30XRb-apA.png" /></figure><h4>Conclusion</h4><p>Navigating the dangerous waters of software development — filled with tight deadlines, rapid changes, and high stakes — can often lead to accumulating technical debt. Fighting it is a continuous journey, not a destination. It’s about making conscious choices and trade-offs.<br>By implementing the right strategies and building a resilient technical immune system, I’ve found a sustainable way to manage technical debt without stifling innovation. It’s all about being prepared and proactive, so you can not only survive but thrive in this challenging environment.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=66130402b375" width="1" height="1" alt=""><hr><p><a href="https://medium.com/similarweb-engineering/navigating-rough-waters-shedding-technical-debt-66130402b375">Navigating Rough Waters: Shedding Technical Debt</a> was originally published in <a href="https://medium.com/similarweb-engineering">Similarweb Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>