Not So Technical Tech Blog

A Work of Art

We already knew how to spread a bad pattern manually. Now we can automate it.

Field
15 min read

It was Friday evening in the office. Everyone was exhausted from the week. But instead of going home to a comfy sofa, we were stuck at this so-called “Townhall.” Supposedly, it was meant to boost morale and engagement.

I was barely standing. My old teammates and I only had one reason to still be there:

Free food.

So we waited. Stared at the stage. Nodded along like we were interested in whatever they were saying.

Eventually, the presentations ended. We drifted over to the buffet line and started chatting among ourselves. No one likes to talk about work after work, so it was mostly mundane stuff. “Is the beef any good?” “Are you taking off next Friday?” Live, Laugh, Love.

Then we sat down at the table.

“I have had enough of this!”

A plate full of food slammed onto the table. It was our lovely former junior.

What the fuck is going on?

We looked at each other, trying to understand what had just happened.

“What happened, sis?” I asked, unknowingly opening Pandora’s box.

The complaints came pouring out.

“I’m losing my mind.”

“Everything is so damn hard.”

“They dumped this random backend ticket on me with zero context.”

“They treat me like I don’t know how to code.”

“They don’t even like the code I write—even when it’s literally copy-pasted from the code right above it!”

And the person asking her to change it was the senior who’d written the code she copied.

So we tried to be good former teammates. Listen to her. Be considerate. Throw out some words of comfort.

“Everyone has their reasons.”

“It’s just the storming phase. Every team goes through it.” (Yes, that was me showing off my Tuckman knowledge.)

And of course, we couldn’t stop ourselves from throwing in a few questions to keep the tea flowing. She kept talking about the team, the environment, the PRs. We kept listening and trying to help.

But you know what? The human mind is a fascinating piece of work.

Even while I was sitting there comforting her, something else was going on in the back of my head. A flashback to when she was on my team.

We struggled at the start.
She barely knew a thing.
I mentored her through every single step.
We worked together, built up her skills.
Until she reached my standard.
Until we, as a team, trusted her to ship without supervision.

So if someone had a problem with her code… they had a problem with my standard.

Maybe they were thinking,

Who the fuck taught this girl?

And just like that, my brain hijacked the issue. She was still across the table venting, but this was no longer her problem.

It was a ME problem.

“Wait a minute. Are you saying I was a trash mentor?”

Everyone froze. Forks stopped halfway to mouths. They looked at me like I’d lost my mind. Then I explained my reasoning:

“If her code is trash, my standard is trash.
If my standard is trash, I am trash.”

The whole table lost it.

“Exactly. It’s because you’re a trash mentor.”

Right. They’re not really your friends if they don’t talk shit about you at a moment like this.

Our junior was the only one trying to defend me.

“No, no! That’s not what I meant at all! It’s not your fault—”

Too late, girl. The ship had sailed.

I shoved my chair back, abandoning my plate of food.

“Let’s see what the code from this so-called ‘senior’ who’s calling me trash actually looks like.”

Yes. Calling me trash. Nobody had said that, but that was where my brain had gone.

Behind me, the table split. There were doubters. There were battle cries.

“Bro, sit down! Your food is getting cold!"
"Go get the laptop! Let’s see this!”

And our poor junior, still trying to explain.

I ignored it all. I was out for blood. I marched across the office to my desk to retrieve my weapon.

The Macalibur.

I swear it didn’t take more than two minutes. And yet, by the time I got back… the table was already cleared. Plates shoved aside. Cutlery out of the way. A landing strip right in the middle for my laptop.

Huh. So it wasn’t just me who was excited to look at some code.

“What’s the repo name?”

She hesitated, pretending to be nice by not telling us. But I knew. She also wanted to watch the world burn.

After a few defensive maneuvers—just enough to make sure she didn’t look like an asshole for selling out her team—we got the name. I typed it in and opened the repo. That was when she gave us the disclaimer:

“The code is pretty old, so you have to cut us some slack.”

Sure, sure. As per the Universal Developer Agreement:

Any code not written by me at the exact moment of discussion is considered legacy and is trash.

On the surface, nothing looked surprising. Just a standard React app. Five years old. Not that bad.

A good old function-based folder structure. Files organized by their technical role: components/, pages/, states/.

If you remember the era when Redux reigned supreme, when we religiously created separate folders for actions, reducers, and every other file type—yes, that one. The kind of setup the ducks pattern tried to get us away from by putting related logic together.

Then I noticed something. This project wasn’t using Redux.

They were using Jotai.


For those of you who don’t know Jotai, it’s another React state management library. But the approach is different.

With Redux, you model a slice of state together with the actions that can change it. Here’s my cart. Here are the actions you can use to manipulate it. Components select what they need from that model, including derived values like the cart total.

Jotai takes an “atomic approach.” You define atoms that can be read, updated, and composed. One atom holds the cart state. Another reads that state and calculates the total. Components subscribe to the atoms they need.

To be honest, they don’t seem all that different to me. Both can achieve the same thing, just in different ways.

But the tools we use can influence how we think about a problem. Sometimes in ways we don’t expect.

ReduxA slice, its actions, and selectorsCart sliceState: items + quantitiesActions: add · remove · updateSelect cart itemsstateSelect cart totalderived valueCart UIJotaiAtoms that can be read and updatedCart atomItems + quantitiesRead · updatereadCart total atomderived valueCart UIReduxA slice, its actions, and selectorsCart sliceState: items + quantitiesActions: add · remove · updateSelect cart itemsstateSelect cart totalderived valueCart UIJotaiAtoms that can be read and updatedCart atomItems + quantitiesRead · updatereadCart total atomderived valueCart UI
Figure 1. Same cart, different ways of modeling its state. Redux selectors and Jotai atoms both provide cart data and a derived total to the UI. Lines show read dependencies, not updates.

Anyway, where were we? Jotai, right.

“Woah, you guys are using Jotai?”

I was surprised. A nice surprise, actually. I’d wanted to see it in a real project.

I’d had a bad experience with Recoil, the state management library from Meta. I didn’t like the syntax or the ergonomics. But I’d heard good things about Jotai and played with it in a small proof of concept, just to get a feel for it. So I was pretty excited to see how a team used it in production.

I opened the states/ folder. A wall of files.

And for legal reasons, let’s pretend this was a generic ecommerce app so I don’t get my ass sued. (The names and code here are simplified illustrations of the pattern, not their actual source.)

The folder looked something like this:

states/
├── productId.ts
├── productName.ts
├── productPrice.ts
├── productDescription.ts
├── productInventory.ts
└── ...

Notice something? No? Okay. Let me open productId.ts for you.

import { atom } from 'jotai'

export const productIdAtom = atom<string>('')

Notice something yet? Still no? Okay. Let me open another file. productName.ts.

import { atom } from 'jotai'

export const productNameAtom = atom<string>('')

One field. One atom.

Okay. But that alone isn’t the problem. You might say:

Hey, what’s wrong with splitting state into atoms? Only the component that needs that specific atom rerenders. That’s the beauty of Jotai!

Ohh, beauty, you say? Let me show you this beauty, then. Here’s what happened when they fetched the data:

const fetchProduct = async (id: string) => {
  const res = await fetch(`/api/products/${id}`)
  const data = await res.json()

  set(productIdAtom, data.id)
  set(productNameAtom, data.name)
  set(productPriceAtom, data.price)
  set(productDescriptionAtom, data.description)
  // The same for the remaining fields.
}

The backend sent a product object.
They pulled it apart, field by field, and put each piece in its own atom.

And then there was a derived atom:

export const productAtom = atom((get) => ({
  id: get(productIdAtom),
  name: get(productNameAtom),
  price: get(productPriceAtom),
  description: get(productDescriptionAtom),
  // The remaining fields go back in here too.
}))

And finally, the product detail page:

export const ProductDetailPage = () => {
  const product = useAtomValue(productAtom)

  return (
    <div>
      <h1>{product.name}</h1>
      <span>{product.price}</span>
      <p>{product.description}</p>
    </div>
  )
}

You see this beauty? Come on. All together now:

What the fuck?

What are we doing here? Splitting particles to power an entire civilization?

We took a backend response that already had the data we needed, in a perfectly usable object.

We split it into individual particles. Then we created another atom to put those particles back into an object… just to render a page that needed almost all of that data anyway.

And the punchline? This wasn’t shared global state used all over the app. No other part of the app needed productDescriptionAtom on its own.

It was data for this page. That’s it.

01 / RESPONSEAPI response{id,name,price,description}02 / SPLITField atomsidnamepricedescription03 / REBUILDproductAtom{id,name,price,description}04 / RENDERProduct pagenamepricedescription01 / RESPONSEAPI response{id,name,price,description}02 / SPLITField atomsidnamepricedescription03 / REBUILDproductAtom{id,name,price,description}04 / RENDERProduct pagenamepricedescription
Figure 2. One product response, split into field atoms, then rebuilt for a page that uses the same data.

I spun the MacBook around so the whole table could see the screen. “Look at this.”

“Why? What’s wrong with it?” our junior asked innocently.

Every senior at the table turned to look at her in disbelief.

”…Everything,” someone whispered.

I didn’t know whether to laugh or cry. Because the thing is, this wasn’t even the lazy way to do it. They’d put effort into this.

This is what happens when people follow the letter of the law but miss the spirit. Jotai says it takes an “atomic approach to state management.” And they took that literally.

To be honest, I blame high school physics teachers. We learned that atoms were indivisible, and somehow that lesson followed us into frontend development.

An atom is the smallest thing, right?

So every string needs an atom.
Every number needs an atom.
Every boolean needs an atom.

But the smallest thing you can split out isn’t always the smallest thing that makes sense to work with.

Here, that could have been the product. An entity. A meaningful unit of data we actually needed together.

I’m not saying you should never split an object into separate atoms. But what were we getting from doing it here?

But no. We all love the hype. We get a shiny new toy, hear the word “atomic,” and suddenly everything has to be an atom.


This wasn’t the first time I’d seen people take a good principle and turn it into team dogma.

Early in my career, I worked on a Ruby on Rails application. Convention over configuration. Put a file in the right place, name it the right way, and Rails wires it up.

But this story is about a different kind of rule. When you’re new to programming and eager to learn, you eventually discover coding principles.

Clean Code.
Clean Architecture.
Clean whatever.

Then come the acronyms.

DRY. YAGNI. KISS. SoC.

You name it. But the one that really got us was SOLID. Especially the S. Single Responsibility Principle.

Simply put: a class should have only one reason to change.

And I assume one of the developers before me really took that to heart. I wasn’t there when they made the decision, but I imagine the thought process went something like this:

We have a product, right? We need to create, read, update, and delete the product.

You with me so far?

If the way we create a product changes, we need to change the code. That’s one reason to change.

But if the way we update a product changes, we also need to change the code. That’s ANOTHER reason to change.

You see where this is going? YES. This maniac followed the principle to the letter.

We had a service for creating a product. Another for reading a product. Another for updating it. And, of course, another for deleting it.

Four fucking services. One method each. Every class now had a single reason to change. Just like the principle told us.

Praise the SRP!

And what were all those files actually doing?

The controller took the request params or body and passed them to the service. The service called Active Record. That’s it.

But instead of keeping those operations in the controller, we had a collection of classes whose main responsibility was to call the next class.

01 / CONTROLLER02 / ONE SERVICE PER OPERATION03 / ACTIVE RECORDControllerparams / bodyActive Recorddatabase operationscreateCreateProductServicecreatereadReadProductServicereadupdateUpdateProductServiceupdatedeleteDeleteProductServicedelete01 / CONTROLLERControllerpasses request params / body02 / SERVICES — ONE PER REQUESTCreateProductServiceReadProductServiceUpdateProductServiceDeleteProductService03 / ACTIVE RECORDActive Recordperforms the database operation
Figure 3. Four services, one method each. For each request, the controller calls the corresponding service, which calls Active Record.

Don’t get me wrong. I’m not against abstraction. Or separation of concerns. Or SRP.

But there is a time and a place. A simple CRUD operation doesn’t become better just because we make it pass through more files.


Anyway. Enough reminiscing. Back to the dinner table.

Even if we’d kept the whole product in one atom, there was still a question. “Okay. But what is this data actually for?” I asked our junior.

We were fetching a product from the backend and displaying it. The user wasn’t editing its name, rewriting its description, or setting its price. So what problem were we solving by putting it into Jotai?

Her former mentor was asking questions again. At a townhall dinner. So much for not talking about work after work.

But here’s the thing. Getting data from the backend sounds simple enough. Call an API. Get some JSON. Show it on the screen.

Until the network fails. Do we retry? When do we refetch? Is this data still fresh? What happens when the user leaves the page and comes back?

There is a whole set of problems hiding behind that innocent little fetch. Enough that we have libraries like TanStack Query and SWR to handle fetching, caching, and keeping server data up to date.

EVERYDAYClothing / T-shirts
Fetched product data
Everyday T-shirt
$24
A soft cotton T-shirt. Nothing complicated.
Current UI state
Size
SML
Quantity
2
Figure 4. On an illustrative product screen, fetched product details and the user's current size and quantity selections have different responsibilities. Changing those selections doesn't edit the product itself.

Putting the response into atoms doesn’t make those problems go away. You still have to deal with them. Except now, apparently, you also have to take the product apart and assemble it again. Why?

I’m not saying backend data is forbidden from entering a Jotai atom. I’m asking why we were using Jotai here.

Imagine a complicated form. Some of its state appears in the navbar. Another part appears in the sidebar. A modal needs to read and update some of the same values. Or the quantity of a product in your cart needs to stay in sync with the quantity shown on the product detail page. Change it here. It needs to match over there.

EVERYDAYCart 3
PRODUCT DETAIL
Everyday T-shirt$24Size M · In your cart
Change the quantity hereQuantity in cart
3
2 → 3
Your cart
Everyday T-shirtSize M · $24 each
The same change appears hereQuantity
3
2 → 3
Subtotal$72
Figure 5. In this single-product cart, changing the cart quantity on the product page updates the cart sidebar and the header's total item count.

Now we have something to talk about. Different components reading and updating related pieces of state. That’s where the bottom-up approach makes sense to me.

But this? We had a product response. And a page that wanted to display the product response. Somehow, we made that complicated.


But there was another pattern in both stories. Not a design pattern. The way this shit spreads.

It starts with one file. A junior joins the team. Gets a task. Looks around for an example. What’s the most obvious thing to do? Copy the closest available code that already does something similar. Understandable.

It’s already there. It works. Someone reviewed it. And who are you to question it? You’re new. The seniors have been here longer. Surely they know something you don’t.

So you copy it. The tests pass. The PR gets approved. Everything you believed about the pattern, confirmed. The next person does the same thing. Now it’s not just one file. It’s a folder. Then it’s a convention.

01 / EXAMPLEAn example is availableIt's already in the codebase.02 / COPYCopy the patternUse it for the next task.03 / APPROVALThe choice looks validatedTests pass. PR approved.04 / REPETITIONMore examples to copyAnother file follows the pattern.A convention takes hold01 / EXAMPLEAn example is availableIt's already in the codebase.02 / COPYCopy the patternUse it for the next task.03 / APPROVALThe choice looks validatedTests pass. PR approved.04 / REPETITIONMore examples to copyAnother file follows the pattern.
Figure 6. Each approved copy becomes another example to copy. Repeating the pattern makes it look more established, without necessarily making it better.

People move teams. People leave. People forget. Eventually, nobody remembers why we started doing it this way. Only that we do. “This is how we write code here.”

Until, of course, you copy the code five lines above yours and get told to change it. Beautiful.

This reminds me of that story where a monkey climbs a ladder to get a banana, only to get beaten up by the other monkeys. The banana is the code. The ladder is the PR. And the monkey? Well, you can guess.

I find all of these fascinating. How easily we become convinced that something is right. And how quickly that conviction disappears when someone else submits the same code.

We wrote it? Looks fine. Someone else copied it? Rejected. Apparently, the code isn’t the only thing we’re judging.

So when something smells wrong, ask what’s actually wrong with it. Is the structure making our lives harder, or is it just not how we would’ve written it? Why is this split into eight classes? Why does this field need its own atom? What problem was this supposed to solve?

Maybe there’s a good reason. Great. Now we know. But “it’s already in the codebase” isn’t the same as “it’s a good idea.”

Especially now that we’re asking AI to write more of that code. You know that file you look at and think:

This is garbage, but it works. I’ll refactor it later.

Or:

That’s legacy code. Don’t copy that.

That distinction might exist in your head. But the AI?

Look at this. A work of art. Let me make sure there’s one in every folder.

We already knew how to spread a bad pattern manually. Now we can automate it.

If we don’t understand what’s wrong with the pattern, how do we explain what we want the AI to avoid? “Write clean code”? “Follow SOLID”? Yeah. We’ve seen how that can go.

Coding agentIllustrative session
You
Refactor the product CRUD code using SRP.
Read create_product_service.rb
Agent
The existing code uses one service per operation. I'll follow that pattern.
Changes
  • Mproducts_controller.rb
  • Mcreate_product_service.rb
  • +read_product_service.rb
  • +update_product_service.rb
  • +delete_product_service.rb
Figure 7. An illustrative agent session: the existing pattern becomes the template for more code.

That’s why I still think we need to keep honing our understanding of software principles. Not just memorizing the names. Understanding what problems they solve. Where they help. When we’re following the words but missing the point.

So we can question the code that’s already there. Guide what gets written next. And spot the thing that’s about to become the next folder full of garbage.

So cheer up, my developer friends. We’re not dead.

(Yet.)

Oh, if you’re wondering about the dinner table, we eventually got tired of talking about code and went back to eating. Found something else to complain about. It’s not like we ever run out of office drama.