Scramblings

Dev scratchpad. Digital garden

Motivation, Productivity Barriers, and Engineering Friction

May 7, 2026 | Reading Time: 20 min

This post is about a pattern I keep hitting while building software.

At the surface, it looks like a motivation/discipline problem. There is a feature to build. I know why it is needed. I may even know the broad solution. But after some point, the mind starts rejecting the work. I open the editor, check the same files, rewrite a prompt, maybe ask an LLM for help, switch tabs, come back, and nothing meaningful has changed.

I do not think this is a motivation/discipline problem anymore. Most of the time, motivation drops after the next step becomes expensive to enter. That expense is usually engineering friction:

  • too much context to hold
  • too many files or systems involved
  • unclear boundaries between systems
  • boring but necessary execution
  • inertia against doing something fully known
  • large generated diffs to review
  • verification work
  • reloading state after a pause or context switch

LLMs did not create this problem. Normal software development already has it. But LLMs can make it worse by producing code faster than understanding can catch up. That was the main point in my earlier post on Codex CLI vs chat workflows . The problem was not just that agent-style tools generated too much code. The deeper issue was ownership. I was left with large patches that looked like progress but did not feel like my design. Reviewing, reshaping, and owning that code cost more motivation than writing a smaller version myself.

So the useful question is not only:

  • can this be generated?

It is also:

  • can I understand it?
  • can I shape it?
  • can I verify it?
  • can I integrate it?
  • can I own it later?

If the answer is no, the generated code is not free progress. It is another form of work.

Where the motivation problem starts

A feature usually does not fail at the start.

The beginning has energy. The problem is new. There is discovery. I read some code, inspect a few paths, sketch a rough solution, maybe ask an LLM for options. That phase usually feels fine.

Then the novelty wears off. Now the work becomes execution. That execution can feel heavy for different reasons:

  • it is overwhelming because there is too much to hold
  • it is boring because the next steps are known
  • it is inertia: I know what to do but still do not want to do it

All three can feel like “I am not motivated”, but they are not the same problem.

A simplified feature flow looks like this:

  • understand the requirement
  • inspect the code
  • design the change
  • implement it
  • integrate it
  • verify it
  • clean up
  • ship or pause

Real development keeps looping inside that:

  • while designing, I discover more context
  • while implementing, I realize the boundary is wrong
  • while using an LLM, I get code that now needs ownership
  • while waiting for an agent, I switch away and lose context
  • while verifying, I realize the intent was not captured
  • after pausing, I have to load everything again

This is where the so-called motivation problem usually begins.

The feature is not necessarily hard in the abstract. The next step is just expensive to enter.

Where friction shows up

1. Execution becomes heavy after novelty fades

Once the interesting part is done, the work can become mechanical:

  • update these call sites
  • rename this shape everywhere
  • add this validation
  • write the obvious tests
  • clean up the generated patch
  • run the boring verification steps

My first instinct used to be to persist. Sit with it. Push through. This seldom works for me.

What usually happens instead is fake work:

  • opening the same files again
  • rereading the same code
  • switching tabs
  • rewriting prompts
  • thinking the same thought again
  • making no real state change

The editor is open, so it feels like work. But the feature has not moved. This distinction matters. If I am not changing the state of the work, I am probably not working. I am hovering around the work.

Sometimes the fix is not more discipline. Sometimes the fix is to make the next step smaller. Sometimes it is to switch work intentionally. Sometimes it is to stop for a bit.

2. Context load becomes the real cost

A feature may be simple in description but expensive in memory. “Add this feature” quickly expands into:

  • what the feature should do
  • what the current code already does
  • which files matter
  • which assumptions already exist
  • what should not break
  • which tests matter
  • what changed recently
  • what still needs verification

A lot of productivity loss is just repeated context loading. This becomes especially expensive as a solo dev because there is no external team memory carrying the state with me. If I pause badly, future me has to reconstruct the whole thing again.

Sometimes the problem is not deep complexity. It is width:

  • too many files
  • too many APIs
  • too many small edits
  • too many possible designs
  • too many things to verify

At that point, the real question is not “can I solve this?”

The question is: can I hold enough of this in my head to make the next move safely?

When the answer is no, the mind starts resisting the work.

This is also where repo-local notes help a lot. In my markdown/git task management post , the main idea was to keep project tasks close to the code. That still feels right. A local todo.md or small markdown note is not just task management. It is an external memory for the work.

For solo development, this matters because the project state otherwise lives in one fragile place: my head.

3. Cross-system boundaries are often the real wall

This is probably the biggest barrier for me.

I have seen this pattern repeatedly: there is code in multiple places, meaning multiple systems that do different things. Each system is heavy enough to have its own nuances. Each has enough API surface to support multiple use cases. The hard part appears when I have to build something that crosses those boundaries.

The solution is not unknown. Most of the time it is some version of:

  • break the problem into sub-problems
  • figure out the interaction points
  • iterate on the separation of concerns
  • decide the smallest end-to-end slice
  • then execute

That sounds straightforward. In reality, this is the work. In cross-system work, the breakdown is the design. Choosing the sub-problems is the work. Choosing the interaction points is the work. Choosing what belongs where is the work.

Before implementation, I usually have to answer questions like:

  • whose responsibility is this?
  • what is the source of truth?
  • what data crosses the boundary?
  • where does validation happen?
  • what should not leak across systems?
  • what happens on partial failure?
  • does anything need retry or idempotency?
  • what is the API contract?
  • what gets logged?
  • what is the smallest slice that works end to end?

Answering these requires knowledge across systems. It also requires focus and tradeoff analysis. By the time I get to a design I am happy with, I am often already tired.

This creates a strange second-order problem. Even after crossing the design barrier, I may not want to prompt the LLM. I already know what will happen next. It will generate code. Then I will have to review it, clean it, reshape it into my intent, integrate it, verify it, and own it. So even “use the LLM now” has an energy cost.

This is why design ownership matters so much. If I give away the boundary design, I am not just delegating typing. I am delegating the shape of the system. And if the generated shape is wrong, the cleanup is not mechanical. It is design recovery.

4. LLMs create false progress when intent is not anchored

A large generated diff can look like progress while actually increasing the work. The problem is not only that there is more code to review. The bigger problem is that the code may be wrong structurally:

  • logic is in the wrong place
  • abstractions are shaped differently from what I want
  • responsibilities are split across the wrong layers
  • data flows through the wrong boundary
  • error handling does not match the real system
  • the code works locally but does not fit production behavior
  • the output captures a plausible implementation, but not my implementation

This is what I saw with Codex-style workflows. With broad repo context and “implement this feature” prompts, the tool often produced a lot of code across many files. On first look, it felt like progress. On review, domain boundaries were blurred, separation of concerns collapsed, and I had to decide whether to refactor a large alien patch or throw it away.

Both options were demotivating. The article on reducing friction in AI-assisted development frames this well: AI assistants are like junior developers with infinite energy but zero project context. If we do not onboard them, whiteboard with them, and give them guardrails, they default to generic patterns. They produce something closer to the average of the internet than the shape of this specific codebase.

That is exactly the failure mode. The model is not an all-knowing, intent-preserving design partner. It is a non-deterministic approximation machine working from visible context. Sometimes that approximation is useful. Sometimes it is good enough. But it does not know my intent, production constraints, domain language, or preferred boundaries unless I make those things explicit. Even then, it can drift. That drift is part of the tool.

If I write code manually, I build understanding while writing it. If the LLM generates the code first, my job changes:

  • understand what it did
  • compare it with what I wanted
  • find where it drifted
  • decide what to keep
  • decide what to throw away
  • reshape it
  • verify it
  • own it later

That is why a large generated patch can feel demotivating. It is not just more code. It is forced ownership of something I did not really design in the shape I wanted.

The safer use is cognitive offloading, not cognitive surrender.

For me, offloading means delegating a bounded part of the work while still staying in the loop. Surrender is when I start accepting external reasoning without doing enough deliberation of my own. With LLMs, that line is easy to cross because the output is fast, confident, and usually plausible.

Plausible is not enough.

5. Agent runs can make context switching worse

Agent-style workflows add another version of the same problem.

The flow often becomes:

  • load context
  • give task to agent
  • agent starts working
  • switch to Slack, Jira, messages, browser tabs, or something else
  • agent returns
  • review diff

This looks efficient. Sometimes it is. But for context-heavy work, it can be expensive. When I gave the agent the task, my context was warm. I knew the files, constraints, expected direction, and tradeoffs. If I immediately switch away, I lose that warm context.

When the agent returns, I now have to do two jobs:

  • reload the original problem
  • understand the generated output

Review becomes more expensive than it needed to be.

For small tasks, switching away is fine. For design-heavy or boundary-heavy tasks, switching away often creates more friction than it saves.

A better use of the waiting time is to stay near the problem:

  • write the test I expect
  • inspect nearby code
  • check call sites
  • note assumptions
  • prepare a review checklist
  • note the next command to run

The point is not to stay busy. The point is to keep review cheap when the output comes back.

6. Verification becomes the expensive part

After implementation, the work becomes verification and cleanup. This part is easy to underestimate because it does not feel like building. But this is where the feature becomes real. Verification includes checking:

  • whether the result matches intent
  • manual behavior
  • edge cases
  • migration safety
  • compatibility
  • tests
  • logs
  • rollback behavior

This matters even more with LLM-generated output because generated code can be locally plausible while still being wrong for the actual environment. Fowler’s April 2026 fragment quotes Ajey Gore’s point that if agents make coding cheaper, verification becomes the expensive thing. That feels right to me. The human job shifts from “can I produce code?” to “can I define and recognize correctness?”

For many real systems, correctness is not one simple thing. It is a set of expectations:

  • this API contract must hold
  • this data must not leak across boundaries
  • this retry must be idempotent
  • this migration must be safe
  • this log must make production debugging possible
  • this UI state must not move into the wrong layer
  • this behavior must survive future changes

Agents can help execute. They cannot fully decide (yet) what correct means for my system.

So instead of asking only “what did I ship?”, the better question is: what did I validate?

That question is much harder to fake.

A useful lens: technical debt, cognitive debt, and intent debt

The April 2026 Fowler fragment also mentions a useful split from Margaret-Anne Storey:

  • technical debt lives in code
  • cognitive debt lives in people
  • intent debt lives in artifacts

I do not want to overdo debt metaphors, but this split maps cleanly to the motivation problem. Technical debt makes the code harder to change. Cognitive debt makes the system harder to reason about because understanding has eroded. Intent debt is what happens when the goals, constraints, and decisions behind the system are not captured well enough in artifacts.

LLMs can increase all three:

  • they can add technical debt by producing poor structure
  • they can add cognitive debt by creating code I do not understand
  • they can add intent debt by implementing something plausible without preserving the actual design intent

For solo work, cognitive debt is especially dangerous because there is no team memory to fall back on. If the design lives only in my head, and I let the code drift away from it, re-entry becomes painful.

This is why names, boundaries, notes, and tests matter. They are not ceremony. They are how intent survives. Good names expose intent. Clear boundaries preserve intent. Tests encode intent. Repo-local notes anchor intent. Without those, every pause becomes more expensive.

What has actually worked for me

The thing that helps most is reducing the cost of entering the next step. The rough operating model I use now is this.

1. Identify the actual barrier

Before pushing through, I try to name what is happening.

Is this:

  • boredom or inertia against known work? If yes, I need less decision-making.
  • too much context or lost context after switching? If yes, I need a context cache.
  • cross-system design? If yes, I need a design note.
  • a large LLM-generated diff or verification load? If yes, I need to break things down.
  • plain fatigue? If yes, take a break and stop pretending discipline will fix it.

Different barriers need different fixes. Treating all of them as “lack of motivation” is too vague to be useful.

2. Make the next step smaller than feels necessary

The next step should be easy to enter. Good next steps look like:

  • inspect one interface
  • decide one interaction point
  • review one generated file
  • write one test
  • update one caller
  • run one command
  • commit one safe slice

If the next step still feels heavy, it is probably not the next step. It is still too large.

For boring but known work, I try to remove decision-making:

  • make a checklist with fewer than five items
  • batch similar edits
  • timebox the mechanical part
  • make small commits
  • use the LLM only for narrow mechanical changes

The point is to turn “I do not want to do this” into “just do these obvious next three things.”

3. Own the design before asking for code

For boundary-heavy work, I try to write a small boundary note before implementation. It usually answers:

  • goal
  • systems touched
  • source of truth
  • responsibility split
  • data crossing the boundary
  • validation point
  • failure behavior
  • retry or idempotency needs
  • smallest end-to-end slice
  • tests or commands to verify

This is not formal documentation. It is a context cache. It also turns vague pressure into concrete decisions. That is useful for me, and it is useful for the LLM if I decide to use one. This is basically the “design-first collaboration” idea: do not jump straight to implementation for complex work. First align on capabilities, components, interactions, contracts, and only then code.

When the design is fuzzy, the generated code will also be fuzzy. It may be large and clean-looking, but it will still be fuzzy.

4. Prime the LLM narrowly

I prefer LLM use where I control the context.

This is why chat+files works better for me than broad agent autocontext for many tasks. In chat+files, I attach exactly the files I want the model to see. I can read the output in one place. I decide what to paste, what to ignore, and how to integrate it.

The model may be the same, but the workflow is different. The important difference is control.

Good priming usually includes:

  • the exact files needed
  • the relevant interface or contract
  • the boundary decision
  • the local style to follow
  • what not to change
  • what assumptions to list

Bad priming is basically:

  • here is the repo, implement the feature

That invites the model to become global. It starts moving logic across layers, inventing structure, and optimizing in places where I wanted stability.

For simple work, broad tools can be fine. For design-heavy work, I want narrow context.

5. Use LLMs as bounded collaborators, not feature owners

I still use LLMs a lot. But I try not to hand them broad design-heavy changes.

Bad shape:

  • implement the full feature
  • modify all relevant files
  • handle the whole flow across systems
  • refactor this and update everything

Better shape:

  • implement only this adapter
  • update only these call sites
  • do not change the public API
  • keep error handling in the current style
  • add tests only for this boundary
  • list assumptions
  • list changed files
  • list things not handled

The narrower the task, the easier it is to judge whether the approximation is acceptable.

My current rough split is:

  • good fit:
    • exploration
    • summarizing code
    • Go tests with strict prompts
    • small frontend components when I own state design
    • DTOs after the data model is already fixed
    • mechanical edits
    • small single-file helpers
  • risky fit:
    • DB schema design
    • cross-layer API design
    • state ownership decisions
    • tooling and CI configuration
    • broad refactors
    • whole-feature generation

This is not a universal rule. It is just what has worked for me. The key is that I need to feel like I still own the design and the code.

6. Treat verification as first-class work

I try to define verification earlier instead of leaving it as a vague cleanup step. Before or during implementation, I want to know:

  • what behavior proves this works?
  • what test should exist?
  • what manual path should I check?
  • what edge case matters?
  • what log would help debug this?
  • what migration or rollback concern exists?
  • what would make me reject the generated patch?

This also changes how I use agents. If an agent is generating code, I can use the waiting time to prepare verification:

  • write the expected test
  • list acceptance criteria
  • inspect call sites
  • prepare a review checklist
  • note suspicious areas to check

This keeps me from treating generation as completion. The feature is not real until I have verified it.

7. Keep context anchored near the repo

For me, this means local markdown notes close to the code.

Usually the note is very small:

  • current goal
  • current decision
  • important files
  • systems touched
  • open questions
  • next step
  • command or test to run

This is connected to my markdown/git task management workflow. I like having a project-local todo.md because it avoids switching to a separate task system. The task file becomes part task list, part design scratchpad, part re-entry cache.

The categories from that system still help:

  • P-0: urgent/current work
  • P-1: important next work
  • P-2: obligations or pushed-out work
  • P-X: excitement-driven work that helps restore momentum

The exact labels matter less than the function. The function is to keep the work close to the code and reduce context switching. For active feature work, I especially care about two small notes.

A boundary note:

  • what are the systems?
  • what crosses the boundary?
  • who owns what?
  • what should not leak?
  • how is failure handled?
  • how do I verify it?

A re-entry note:

  • what changed?
  • what is still broken?
  • what was I thinking?
  • what looked suspicious?
  • what is the next step?
  • what command should I run next?

A good re-entry note is one of the highest-leverage habits I have found.

Sometimes the remaining work is only thirty minutes. But without a note, it needs forty minutes of reload before I can even begin.

8. Build a small feedback flywheel

One useful idea from the AI-friction article is the feedback flywheel: when AI collaboration fails in a repeated way, capture that learning and feed it back into the workflow.

For solo work, this does not need to be elaborate.

If the LLM keeps making the same mistake, I add a short note:

  • do not put business logic in UI helpers
  • do not change this public API
  • keep validation in this layer
  • use table-driven tests
  • use stdlib only
  • list assumptions before code
  • ask before moving state ownership

If a prompt works well, I save the shape. If a review catches the same issue twice, I add it to the checklist.

This is how my preferences become usable context. Otherwise I keep paying the same review cost again and again.

9. Switch intentionally when persistence fails

If brute persistence is clearly not working, I switch intentionally. For me, that usually means picking something from P-X or from a pushed-out section in my project notes. Other options that often work are:

  • bounded internal refactoring
  • something I had postponed earlier
  • a small maintenance task
  • even a lower-priority task if it gets movement back

Sometimes a lower-priority task is better than sitting in fake work for hours. This is not random avoidance. The difference is whether I am preserving the ability to return cleanly. If I leave a re-entry note, switch to a bounded task, and come back with context intact, that is controlled switching. If I just disappear into tabs and return with no idea what I was doing, that is avoidance.

10. Step away properly when the issue is fatigue

Sometimes switching work is not the right answer. Sometimes the correct thing is to stop.

If I am opening the same files, rewriting the same prompt, and changing nothing, I probably do not need more discipline. I need a real break.

The best version of that is:

  • leave a re-entry note
  • note the next command or test
  • move away from the screen
  • walk, eat, drink, reset, or sleep

Stopping cleanly is part of the work. A clean stop plus a good note is much better than dragging a tired brain through half-review work.

Closing

I started thinking about this as a motivation problem. Now I think motivation is often downstream of engineering friction.

When the next step is small, clear, and easy to verify, motivation is much easier to find. When the next step requires loading multiple systems, designing interaction points, reviewing a large generated patch, remembering old decisions, and re-entering after a context switch, the mind naturally resists.

LLMs did not create this whole problem. But they can make parts of it worse. Not only by generating more code, but by generating code that is plausible while drifting from intent, structure, and real production behavior.

The useful response is not to reject LLMs. I still use them a lot. The useful response is to keep design ownership:

  • prime the model with narrow, relevant context
  • design boundaries before asking for implementation
  • keep generated diffs small
  • use LLMs for bounded work, not broad feature ownership
  • treat verification as first-class work
  • anchor decisions in repo-local notes
  • switch intentionally when persistence is failing
  • step away properly when the real issue is fatigue

For me, the goal is not to stay motivated all the time. The goal is to keep the work easy enough to enter, clear enough to verify, and cheap enough to re-enter.

References