# What we optimise for when we reach for an LLM

> Source: <https://exploring-better-ways.bellroy.com/what-we-optimise-for-when-we-reach-for-an-llm.html>
> Published: 2026-08-29 10:54:09+00:00

**Disclaimer: I wrote this blog post entirely by hand, but used Claude to review and provide feedback
on it before seeking human review.**

Most of the blog posts I read about LLMs are either gushing about a new era of infinite productivity, or bemoaning the enshittification of the entire software industry. I don’t often come across posts that take the even-handed view of evaluating these tools like any other tool, so I thought it was time to make a contribution to the discussion.

If I discovered one of our engineers repeatedly typing out boilerplate for JSON decoders, I’d be disappointed. If I discovered them spending an afternoon hand-crafting types that encode our business invariants into our systems and then letting LLMs draft the feature based on those types, I’d be delighted. The aim of this post is to communicate why I’d react differently in each circumstance.

## Our context (and why yours may differ)

The lens I’m bringing to this has been shaped by the kind of company Bellroy is and the kind of CTO I strive to be. Bellroy is not a software company; it’s a physical-product company that happens to have technological proficiency as one of its core competencies. Since the very early days, Bellroy has employed an internal engineering team - initially to focus on automating the order fulfilment pipeline, but today to build and maintain an entire bespoke e-commerce platform. Bellroy has been continuously profitable since early in its history and isn’t sprinting towards the end of a funding runway. This substantially influences the way decisions are made day-to-day. One of Bellroy’s goals is sustainable, ethical profitability. To meet that goal - among other things - we must be evaluating choices based on long-term Return On Investment (ROI). That evaluation does a lot of heavy lifting: it means that the benefit of shipping more code more quickly must be offset against the cost of maintenance. This philosophy neatly fits my approach to running the Technology team at Bellroy: we must operate as a profit centre, not a cost centre.

## Engineering as a profit centre

Some concrete examples of “profit centre” thinking in action:

[choosing statically typed functional programming languages](/our-technology-stack-and-how-we-got-here.html)to eliminate entire classes of production issues- prioritising regular dependency updates of every single code repository we manage to avoid “drop everything” zero-day vulnerability scenarios and dependency rot
- employing
[a “fixed time, flexible scope” project methodology](/using-shape-up-what-works-what-weve-changed-whats-next.html)to eliminate project “overhang” while still delivering tangible, measurable outcomes - using internally-built probabilistic
[Net Present Value (NPV)](https://www.investopedia.com/terms/n/npv.asp)calculator tools that use Monte Carlo simulations to evaluate project benefits (another blog post, perhaps), and using those valuations to drive prioritisation decisions and push back on scope

This last point is important - we rely on our engineers not only to ship features and maintain our
codebases, but also to help project stakeholders to identify what they actually *need* as opposed to
what they asked for. Technology does not operate as a silo in Bellroy; we collaborate with
other functional teams to deeply understand their processes - and how changes to their processes
impact the rest of the business - before we write a single line of code.

This is where we find the current generation of LLMs still wanting. The “everyone’s a builder now” mentality sounds great if you believe that generating code is all benefit and no cost, or if it lets someone adept at business analysis - who has already validated the ROI of a particular change - bypass the engineering department and ship the thing themselves. That perspective ignores the opportunity cost of the next-best-thing that person could have been doing with their time, and the maintenance cost of the thing that the Technology team now maintains but had no part in designing.

“But Mike”, I hear you say, “surely the LLMs can do the maintenance and bug fixes for all those
vibe-coded features as well?” Well, yes, you’re (partially) right. We *do* use LLMs for maintenance
and bug fixes. But what we don’t do is rely on LLMs to resolve those rare-but-critical production
issues at 3am on a Sunday morning. With [so many horror stories](https://www.theguardian.com/technology/2026/apr/29/claude-ai-deletes-firm-database)
floating around the internet of what unmonitored LLM tools can do with access to production
infrastructure, we are unwilling to give them (write) access to ours. With this responsibility still
in the hands of the humans, the humans need to have a good mental map of our systems architecture
and our infrastructure. We need to be tracking how big the on-ramp is for new staff. And that’s
exactly what we’d give up by ceding all engineering activities to the LLMs.

## What maintainability actually means to us (and Claude!)

We use [Claude](https://claude.ai/) at Bellroy, across the entire company. And it’s great for so
many things.

One thing we’ve noticed about Claude-generated code is that Claude tends to want to provide verbose comments on most changes it makes. Despite explicit instructions not to do so, it uses these comments to document what the code does, or adds context from the prompts, or refers to previous states of the code that become irrelevant the second a change ships.

For us, this is bad practice. Code comments should only provide context that is current and that is
*not* derivable from just reading the code - adding a comment that just describes what the code does
increases the risk of the documentation diverging from the implementation, which just adds potential
confusion for no real benefit. Our function, type and variable names should do enough of the work to
allow an engineer to get oriented.

We use comments only to provide the “why” of a particular implementation choice so the *intent* -
the “should” of the change - is preserved, because it is that information that is hardest to
reverse-engineer from the code one year from now. We take a similar approach to pull request
descriptions, using a template that asks for a problem statement (what problem exists prior to this
change), a solution description (how this particular change solves the problem) and an account of
what the engineer has done to verify that the problem is actually resolved. Without engineers having
taken part in the scoping process and understanding the size and shape of the business problem, they
can’t provide that context.

Another deliberate decision we’ve made to assist with maintainability is to work in a [monorepo](https://monorepo.tools/).
This means that in our commit history, related changes are often shipped together. Coupled with the
practices described above, this - at least in theory - should mean that an engineer working in a
particular area should be able to answer 3 questions:

- What does this particular area of the code do?
- Why - from a business perspective - does it do it? And why in this particular way?
- What else had to change to make this solution workable, and <repeat steps 1-3 for those other related areas>

## Where LLMs earn their keep

Ironically by having our humans care about and document this stuff we can provide better context to LLMs. This makes them more efficient at feature building, bug fixing and triage.

LLMs are great, and you should use them! But - in my opinion - they are *best* employed producing
what can be machine-verified. There are still many things better left to human judgement.

An example: writing a mechanical JSON encoder and decoder to match a wire format, repeatedly and by
hand, is not something any human engineer should be doing in 2026. Declining to automate that is
paying a tax for no good reason. In Haskell, we have a standard practice for testing such things - a
[golden test](https://hackage.haskell.org/package/tasty-golden)
with real examples to anchor you to reality, and [roundtrip property tests](https://hackage-content.haskell.org/package/hedgehog-1.7/docs/Hedgehog.html#v:tripping)
using hundreds of randomly generated values each time to catch edge cases. Provide the LLM with the
real examples and let it do the work of writing the encoders, decoders and tests (and for the
record, there are better tools than LLMs for deriving correct marshalling and unmarshalling
strategies for JSON wire formats from [OpenAPI](https://www.openapis.org/) specifications - I merely
use this as an concrete example of boilerplate code).

A rule like “all currency amounts on a sales order are in the same currency, across its lines and
associated payments” is something you can - and we do - encode in our type system. This means a
program that mixes currencies in one order is *unrepresentable in valid code*; it simply won’t
compile.

(An aside for the Haskellers - here’s a sample of Haskell code showing how that’s achieved using a
phantom type index on our internally-defined `Discrete`

type, a [singleton type](https://hackage.haskell.org/package/singletons)
to reflect it at runtime and an [existential](https://wiki.haskell.org/Existential_type) unwrapped
at serialisation boundaries - a simplified version of a mechanism from [ safe-money](https://hackage.haskell.org/package/safe-money)
and probably worthy of its own blog post):

```
newtype Discrete (currency :: Currency) = Discrete Integer

data SomeDiscrete where
  SomeDiscrete :: forall currency. Sing currency -> Discrete currency -> SomeDiscrete

-- the `currency` type argument enforces a common currency across attributes
data OrderItem (currency :: Currency) = OrderItem
  { code :: AccountingSystemIdentifier,
    amount :: Discrete currency,
    description :: NonEmptyText,
    quantity :: Natural1,
    taxAmount :: Maybe (Discrete currency),
    taxCode :: TaxCode
  }

fromBaseUnit :: forall (currency :: Currency). Integer -> Discrete currency
fromBaseUnit = Discrete

instance FromJSON (OrderItem currency) where
  parseJSON = Aeson.withObject "OrderItem" $ \obj -> do
    -- ...
    amount <- fromBaseUnit <$> obj .: "amount"
    -- ...
```

But it’s not obvious to a machine - and frankly most humans, unless they’ve done the research -
*why* you’d want to do such a thing. For some businesses, like ours, it’s a fundamental truth
because of the way we’ve set up our platform. For others, they may be far more flexible about how
they take payments. And it may change in the future, which should influence exactly how you
implement it. And all of those factors I just described are not the sort of thing an LLM will intuit
by default (yet), unless the person operating the LLM goes to great lengths to include that in the
context. By having that rule encoded - with care and deliberate design - in the type system, you’re
creating an environment where you can delegate smaller coding tasks to the LLMs with confidence.
Where the fundamentals of the business are unbreakable contracts the LLM has to respect.

This combination of determinism and non-determinism is where you can have your cake and eat it too.
Consider the problem of wanting to have an LLM triage an email inbox and auto-respond. Emails can
come from anywhere and go to anywhere, and I would lose sleep at night just plugging Claude into
[Gmail](https://workspace.google.com/intl/en_au/products/gmail/) with a set of instructions and
saying, “have at it”. What I’m much more comfortable with is having a workflow automation tool like
[n8n](https://n8n.io/) pull each email requiring triage using the Gmail API, feed the email headers
and body into an LLM whose only task is to choose the action to take from a finite list, and then
having n8n execute that action via the Gmail API. The humans retain complete control over the input
and possible outcomes, while the LLM is doing the previously-impossible task of dealing with every
permutation of email content you can throw at it and turning that into something sensible.

## A tool, not a strategy

Claude has been adopted across Bellroy and most Bellrovians use it every working day. They’re using
Chat, Cowork and Code, or plugging in other tools into Claude’s API, such as n8n. I spend a good
chunk of my working hours each week trying to work out ways we can more effectively leverage this
technology right across the company - not just in the Technology team - and at the time of writing
we’re actively hiring an [AI Enablement Lead](https://bellroy.com/careers) (among other roles -
we’re growing!). These tools are game-changing. I intend for Bellroy to remain an AI-forward
organisation and we currently have more great ideas than we have people to execute on them.

What these tools have done is change the distribution of costs, and that’s going to mean different
things to different companies depending on their strategy. The cost of building things -
ideas, processes, software tools - has clearly gone down, and to some extent the cost of maintenance
has as well. But I would argue the cost of maintenance has not reduced to the same extent that the
cost of building has, and in order to effectively manage that maintenance burden you need humans to
understand the why and how of the things we build. For the time being - to avoid accumulating
[cognitive debt](https://www.media.mit.edu/publications/your-brain-on-chatgpt/) - Bellroy’s human
engineers will still be writing code by hand… some of the time.
