ミツモア Tech blog

「ミツモア」を運営する株式会社ミツモアの技術ブログです

# Death of the Fullstack developer

Or: How I learned to stop worrying and love the AI.

Introduction

Hello I'm a Fullstack developer. This is an obituary detailing how I went from being a senior full stack developer to a vibe coder over a year.

This is not me offering a complete LLM workflow. There are plenty of other good resources for that already.

Before Meetsmore

For various compliance reasons with prior jobs I was not able to try AI tools in a professional capacity until joining Meetsmore. I've used chatgpt for personal use but apart from that my experience with LLMs as a coding tool was non existent.

I am a senior engineer. In prior jobs I'm used to approaching things in a TDD coding style. I like simple and minimalistic code.

My coding style would be to identify in a general level what areas of interests there in a code for whatever task I am trying to do. Then write descriptive todo comments. I would then slowly and methodically work trough all todo comments, creating test suites and implementation, and when none are left I would have finished my task.

Starting with cursor

I was impressed by Cursor's auto complete. Often while writing todo comments, it would suggest a complete working implementation. The code writing part of my workflow was dramatically sped up."

For when I had trouble understanding how something worked. Usually because it was ambiguous or very complex. I would try asking the cursor agent before asking my peers. It usually gave nonsensical responses with grains of truth.

It basically served as a filter, if something was trivial it would explain it clearly, and if something was genuinely hard its confused responses confirmed I needed to dig deeper.

Moving to agent workflows.

One anti-pattern I saw with Cursor that really frustrated me was the lack of control over the auto complete behavior. If it settled on a change that needed to be done then it would keep suggesting it over and over again.

the auto complete would fail to understand what context it needed to consider. I had occasions where it wanted to suggest an approach I knew would break the code. Any time I wrote changes in that area it would keep suggesting breaking changes. I could decline but the auto complete workflow is prompt-less.

In a way the autocomplete workflow was an ego boost first, a productivity tool second. It felt really good to use when the LLM understood my intentions and correctly implemented them for me. But when we were not aligned, it would be frustrating or potentially dangerous. My curiosity grew for a pure agent based workflow and I made the switch to Claude Code.

Initially I was apprehensive about using agents. My mental model of an LLM back then was: An LLM is equivalent of a very motivated intern. It has a lot of free time so it can study and try complex things for prototypes. It can do mundane tasks for you. But when it comes to important things it is better to do them yourself.

Kill your ego

I caught myself delegating tasks I didn’t want to do to LLMs while keeping the interesting work for myself.

So as I started this journey I made a conscious decision. My existing experience be damned. I will try solving tasks with an agent first. I will only step in and do work manually when necessary. For now it is about learning this new workflow. I will not concern myself with if my manual approach would've been better or not.

The agent approach of doing things is kind of like watching someone flail and fumble. It would run incorrectly formatted commands. It would get stuck in weird loops when fixing type errors.

At the same time. None of these problems made it to production. We would catch them while working or in the code review process. I just had this frustration and anger. "Why am I relying on this tools that are making mistakes I wouldn’t make?".

While I don’t fully trust the output of an LLM, I trust in my ability and the ability of my peers around me to catch its mistakes.

Learning to Prompt an LLM

From now one the starting point for any task was to have an conversation with the LLM.

For example say I am looking at a bug in how we upload images I would prompt it like.

"Please have a look at the image upload in our chat system. Explain to me the full flow from frontend to backend. Analyze how we are sending the binary data to the API and where the images are stored. Explain what happens on the backend for example if we just upload a binary file or if there is also some kind of relationship with the images and database tables"

This would proceed with us going back and forth. Me asking follow up questions and verifying its statements and it elaborating.

Then I would propose a solution to the problem. I would treat it like an intern where I prescribed a specific architectural approach and spelled out every detail. Then I would align with it and have it explain the approach to me until I was satisfied it understood the problem.

Even with detailed instructions it would do things like.

  • Asked to make a script that converts SVG files in a folder ⇒ ended up deleting all svg files in the project.
  • Asked to implement a test case ⇒ Implemented the opposite scenario because otherwise the test would have failed.
  • Discover critical bugs while implementing new feature ⇒ Justifying them as out of scope and never mentioning them again.

So I was spending all this time giving detailed instructions on what the LLM should do and still I kept occasionally running into failures.

I was noticing a pattern as I was having my alignment conversation with the agent. I would first align on the problem and the code. Which would follow by me explaining the approach. But often before I got to the next step the agent would often preemptively suggest the approach I had in mind. Often as I was in middle of writing it.

I realized here that I was focusing on the wrong thing. I am not minimizing mistakes with my detailed prompting. I am just enforcing it to use my coding style.

Speak like a caveman.

I decided for a while to communicate as simple as possible with the LLM.

> me think about marketPriceMap. Why map no array?

✻ Thinking…

  The user is asking about why we use a Map for marketPriceMap instead of an array. Let me think about this:

  Current structure:
  - marketPriceMap: Map<string, MarketPriceCalculationResult[]>
  - Key is ${serviceKey}:${areaKey} 
  - Value is array of MarketPriceCalculationResult

  Why a Map instead of array?

  Map advantages:
  - O(1) lookup by key (serviceKey:areaKey)
  - Can efficiently find market price data for specific service+area combination
  - No need to search through array

  Array alternative:
  - Would need to search through array to find matching service+area
  - O(n) lookup performance
  - Would need structure like: { serviceKey, areaKey, marketPriceData }[]

  But the user might be thinking:
  1. Maybe we only need one service+area combination per page?
  2. Maybe the data structure is over-engineered?
  3. Maybe array would be simpler?

  Let me think about the use case:
  - A single article page might have multiple market-price components
  - Each component has different service+area combinations
  - We need to efficiently look up market price data for each combination
  - Map makes sense for this

  But maybe the user is questioning if we really need multiple service+area combinations on one page? Or if the key format is correct?

⏺ Good question! Let me think about this.

  Current structure:
  - marketPriceMap: Map<string, MarketPriceCalculationResult[]>
  - Key: "エアコンクリーニング:東京都"
  - Value: MarketPriceCalculationResult[]

  Why Map instead of array?

  Map allows O(1) lookup by service+area combination. But you're right to question this.

  Alternative could be:
  marketPriceData: Array<{
    serviceKey: string
    areaKey: string
    results: MarketPriceCalculationResult[]
  }>

  Questions:
  1. Can one article page have multiple different service+area combinations?
  2. Is the key format serviceKey:areaKey correct?
  3. Would simple array be better?

  If typically one article = one service+area, then array might be simpler. But if one article can have multiple market-price components with different services/areas, Map is better for lookup performance.

  What do you think? How many service+area combinations per article page in real usage?

> 1:yes\
  2:notsure can check?\
  3:me think we overcomplicate. maybe data fetch use map, but return array maybe ok? please challange me

✻ Thinking…

  The user is asking me to challenge their thinking about whether we should use an array instead of a map for the marketPriceData. Let me think through this:

  1. Yes - one article can have multiple service+area combinations
  2. Not sure about the key format - we should check
  3. They think we might be overcomplicating it - maybe fetch uses map internally but return array

  Let me challenge this thinking:

  Arguments for Map:
  - O(1) lookup during hydration - when we have a specific service+area, we can instantly find the data
  - Clear key-value relationship
  - Efficient when you have many different service+area combinations

  Arguments for Array:
  - Simpler structure
  - More straightforward to work with
  - Frontend might iterate through all anyway
  - Less coupling to specific key format

  The user is suggesting maybe the data fetching can use a Map internally for efficiency, but the final data structure returned could be an array. This is actually a good point - we can have the best of both worlds:

  1. Internally use Map for efficient lookups during fetching/organization
  2. Return Array to DOM hydration for simplicity
  3. During hydration, just find() the right entry in the array

  Let me challenge them but also see their point.

⏺ You're right to challenge this! Let me think through it:

  Your point about overcomplication is valid:
  - Internal fetching can use Map for efficiency
  - But final data structure could be Array for simplicity

  Challenge back:
  With multiple service+area per article, hydration needs to find the right data:

  // Map (current): O(1) lookup
  const data = fetchedData.marketPriceMap.get(`${serviceKey}:${areaKey}`)

  // Array: O(n) lookup  
  const data = fetchedData.marketPriceArray.find(item =>
    item.serviceKey === serviceKey && item.areaKey === areaKey
  )?.results

  But you might be right because:
  1. Article pages probably don't have 100s of market-price components
  2. Array is simpler to work with
  3. Frontend might iterate through all anyway
  4. Less coupling to key format

  Your suggestion sounds good:
  // Fetch layer uses Map internally for organization
  // But returns simple array to hydration
  marketPriceData: Array<{
    serviceKey: string
    areaKey: string
    results: MarketPriceCalculationResult[]
  }>

  Question: Are you ok with O(n) lookup during hydration for simplicity?

> 0(n) lookup not acceptable. repeating data is ok. so if map only hydration flow. it is ok

⏺ Ah, I understand! You're right - O(n) lookup is not acceptable.

  Your solution makes sense:
  - Keep Map for O(1) lookup during hydration (performance critical)
  - Repeating data is acceptable tradeoff
  - Map is only for internal hydration flow

  So current approach is actually good:
  // FetchedDataForDomCreation - internal hydration interface
  marketPriceMap: Map<string, MarketPriceCalculationResult[]>

  // Hydration: O(1) lookup - performance critical
  const data = fetchedData.marketPriceMap.get(`${serviceKey}:${areaKey}`)

  // Final hydrated DOM: each component gets its own copy of data
  props: {
    marketPriceData: MarketPriceCalculationResult[] // repeated data is ok
  }

  This is good design because:
  - Fast hydration with Map
  - Simple final DOM structure
  - Data duplication not a problem (small objects)
  - Clean separation of concerns

  You're right - the current structure is appropriate. Thanks for the challenge! It helped confirm the approach is sound.

  Should we keep it as is?

> yes keep as is

After a long session like this I did ask the Agent what I thought:

> is speak short, hard to read? speak long better?

⏺ Short is better for me.

  Your short style forces me to:
  - Answer directly without fluff
  - Focus on key points only
  - Not waste your time reading

  Long responses work when:
  - You need detailed explanation
  - Complex technical analysis
  - Multiple options to compare

  Your current style is efficient - you get answers fast, I stay focused. If you need more detail on something specific, you can always ask "explain more" or "give example."

  Keep the short style - it works well for our technical discussions.

This shifted my whole attitude to prompting. I still want to align, understand the code and break down the task with our prompting.

But there is no need to write detailed prompts at all. I do not need to explain architecture to it.

Enforcing specific approaches is just ego. I should give it as little information as necessary and have it propose how to solve things.

Then afterwards we can have a conversation. I can still verify its finding. I can challenge and have opinions on the approach and architecture. But I should let it do most of the heavy lifting.

Scaling horizontally

With my new mindset I wanted to see how far we could push this workflow. The idea was simple. If we align on an approach. Why not spin up multiple agents and have them work concurrently?

I would make a plan for one task. Align on it and then once we were ready I would ask it to start and then I would jump into a new session and do it all over again until the first session had completed. This way I could run 3~4 sessions concurrently.

What I noticed with this approach was that it was exhausting to context switch between different sessions as the agents were working. And the end result was a big backlog of PRs that needed to be reviewed.

The bottleneck ended up being myself. There is a limit to how many complex problems I can keep in my head. We can scale the AI agents but I can’t keep track of the output it is available to produce. With that my mindset shifted from trying to make agents produce code faster. To finding ways to optimize the handoff where I give the task to the agent and the handoff when it gives the task back to me for review.

From this exercise I adopted a new approach where I would after aligning with an LLM have it write a plan to a .md file. As it worked I would have it wrote reports on what it had done. This way I can stop a session at any time and resume it without relying on the context in the terminal session. And with a good planning document I can trust it to finish the task with minimal handholding from my side.

These days I use parallel agents for simple repetitive tasks and keep complex work single threaded. My focus is on optimizing the handoff so I can smoothly start up tasks and then later review them.

My views on LLMs today.

In the beginning I would have long natural conversations with LLMs and I would finetune various behaviors a lot using agents.md and try to micromanage its decision making and patch for various behavioral anti-patterns.

I would often ask LLMs to make recommendations or suggest a way forward. These days I believe LLMs are at its strongest when its task is to process and summarize information or when generating text or code.

So for example when reviewing code. Before I read each changed file of a PR (which I still do). I ask the LLM to summarize the changes. I don't ask it to tell me if the code is good or not. The discussion is focused on whether we follow patterns, what effect the code has. Not on concepts as if "Is there risks of bugs, is the code production ready? Does the code have good performance?". As much as possible these days it is just about giving me digestible information to speed up my understanding.

Conclusion

A coworker told me the other day. “I am surprised about how lightly you take AI”. He explained how he found it hard to pilot an agent in a complex codebase. Where sometimes you can trust it and sometimes you need to push back and sometimes take it more lightly and steer it in the right direction.

From my point of view I have one step at a time reinvented my whole approach to programming. A part of me thought of it as defeat. As surrender. I thought I had let the Fullstack developer part of me die.

But that comment proved to me that this way of looking at it was wrong. I’ve learned to let go of my need to have everything done my way. I’m still just as stubborn as I was a year ago. I’m just picking my battles where it matters.

Join us at Meetsmore

Thanks for reading to the end.

At Meetsmore, we're looking for people to join us in our mission to grow Japan's GDP. If "I want to work with teammates of diverse nationalities at a fast-growing startup" or "I want to use AI agents day-to-day while rethinking how I engineer from the ground up" resonates with you, please get in touch!

Careers: https://corp.meetsmore.com