Research Journal – Syntax Trees and Agents — November 19, 2024

Research Journal – Syntax Trees and Agents

After my experiments with code generation, graph traversal for outside-in prompting, and combined development time and runtime, I was really taken with graph traversal.

But I realised I didn’t need my custom graphs to traverse code – I was working in TypeScript, and its compiler already understands the code as a syntax tree.

Project 2 was orchestrating outside-in graph traversal for code generation, using syntax trees of your existing code to find entry points for generable software. In this approach, an entry point is identified and described briefly with a special type that extends a custom Generable type, a VS Code extension detects and implements Generables, and a tiny custom dependency injection framework injects them at runtime.

You can use code you didn’t write yet and let the system implement in the background. No need to write special graphs – if you’re writing code, you’re already writing a graph. What a great design language for engineers! If you can get the system to use the same type-based delegation recursively, maybe you can specify pretty challenging high-level Generables in a high-level orchestration layer, and use it while it gets implemented underneath you.

But developments in agentic approaches made me feel that I was still undershooting the future – that this would be superseded by an agentic, general purpose software engineer, even though that is a much harder version of the problem to solve! And I wasn’t ready to let go of combining development time and runtime.

More on those spaces and how they influenced my work in the next post 🙏 Including some specific tools I have enjoyed, for people who want to try things out.

Research Journal – Graphs & Runtime — November 15, 2024

Research Journal – Graphs & Runtime

I want to share a story that challenged my most basic ideas about software engineering.

Months ago, as part of my first new AI project, I designed a DSL for expressing software designs. I found a pretty friendly way to describe a pseudo-graph of operations and data. You send it to my server, with a target node.

My server builds outside-in to reach the target node. At each node, it prompts a model to implement a suitable function, with the code for already-implemented consumers (the node’s parents in the graph) and a plain English description (from the DSL) supplied as system instructions. Engineers who work outside-in and consumer-first might already know why this is a useful approach for AI.

You can design and run complex things neatly and correctly – take-home programming exercise level stuff. Though I’m working on something else now, I still love that project. I’m proud of two decisions in particular.

First decision: Pass the graph you want to execute and a target node in at runtime and get the result synchronously – not the software, but the result of running it, providing inputs as the outermost edges of your graph. The server doesn’t build software for you, it computes for you, with software that it builds incidentally to get your result. This means there is no separation between software development and operation – only latency and caching.

It is a shock to work with something that does lots of your job at runtime. Our code is usually so precious to us. Think of the care we take in its production, testing, and deployment. What a strange feeling, to suddenly think of it as so trivial that it might not even be worth caching!

Second decision: Let the client patch their graph – just keep adding to it in separate HTTP calls, specifying whatever parts are needed, referencing parts specified earlier, all while targeting a node in each call to get results.

I used it super-iteratively, designing software with a tight feedback loop between my requests and the resulting responses and software until I had a system I liked. At that point, I could delete the graph and any code, because my requests essentially were my software. Everything after design was incidental – generable at runtime. I could use my honed set of graph-segment-and-target-node patch requests like endpoints to operate the system, even if the system didn’t exist yet. It was a Keanu “woah” moment for me.

I thought deeply about this paradigm we are swimming in – where software must be developed and deployed well in advance of being executed. It felt like seeing water.

We could have a dynamic runtime where models run code ad-hoc to achieve their goals. Code can be used, rather than developed, deployed, and executed, collapsing the space between development and runtime as my project did. There’s no technical obstacle, really. We just need time, compute, and creativity.

That glimpse of the future was really impactful for me, and I took the lessons in my following work.

Research Journal – Missing the AI Forest for the AI Trees — November 12, 2024

Research Journal – Missing the AI Forest for the AI Trees

I’m lucky to have been able to throw myself headfirst into AI since leaving SadaPay, but almost everybody I know in-industry is still building traditional software. There’s a lot happening. I’m working on my fourth AI project now, and I’m so excited by the work we’re doing in this space. You’re going to see a whole new generation of user experiences. I’ll try to pause and share back sometimes, from now on.

Sometimes when I talk with people about AI, all they can share is that they aren’t impressed with ChatGPT. To me, that reaction seems like seeing electricity power the first electric light bulb, and not being able to imagine an air conditioner, a television, an electrified subway system – or maybe not even a better lightbulb – and thereby concluding that candles aren’t going anywhere.

It’s not that skeptics are wrong, it’s that they’re missing the forest for the trees by evaluating a fundamentally new form of compute – an enormous foundational breakthrough – as if it were a product, based on the first product(s) they have seen it power.

We have some form of intelligence on demand now, like electricity, but electricity required a lot of work to go from the first lightbulb to the massively electrified society we have today. We are building a whole new ecosystem in massive parallel, from chips to user-facing software, and we are really just getting started, but it’s coming faster than you might think.

Behavioural Coupling is a Silent Killer — December 14, 2023

Behavioural Coupling is a Silent Killer

People worry a lot about static coupling, but not nearly enough about behavioural coupling.

Coupling in your code is easy to see and even measure. You can tolerate quite a lot of it, as modern IDEs are so good at helping us understand what’s going on.

Conversely, behavioural coupling is a silent killer. People usually introduce it with good intentions, not realising the damage they’re doing.

Good intentions go astray because this particular misstep often feels like a eureka moment. In a flash of insight, you realise that because some external system – which could be another unit of code or a distant microservice – works a _certain way_, you can solve some problem very easily in your system by _just doing x_.

Of course, this kind of thing makes us feel smart. We are using holistic knowledge of our software (and problematically, other people’s software) to save effort – which feels like efficiency – and produce briefer solutions, which feels like elegance.

But if our solution was formulated based on knowledge of how external systems behave, it will intrinsically depend on those behaviours and therefore be coupled to them.

Worse still, this coupling will be invisible. You won’t even know where to look for it. It might be in a simple function call, or hiding in complex patterns of message publication and consumption spanning major organisational boundaries.

Such solutions sit around like landmines. Every change in behaviour starts to feel risky, because you don’t know where it’s safe to step.

I’ve figured out how I have been avoiding this, but maybe it will sound a bit weird.

When I’m writing a unit of code, I’m _inhabiting_ it. I think and talk with my pair about what I – as the unit of code – am able to know through properly defined interfaces. I’m not allowing things outside of that to interfere with my behaviour (the behaviour of the unit), because I (as the unit of code) just don’t know about that.

For example, if my pair wants to assume a string passed to some public method will never be empty (say, because the current consumer is getting the value from validated user input with a minimum length), I put my cursor inside the method and say “Well, we (as the method) don’t know that right? We just know it’s a string.”

It’s important that our solution not rely on information that is known to the programmer _but not to the program_, because it could lead to – in this case – an implementation that explodes when an empty string is provided, which of course could happen at any time through a new consumer or a change to the existing consumer.

These things we feel clever about knowing and leveraging aren’t safe to build software on. They are and should be shifting ground, because behaviour should be easy to change.

Maybe this practice sounds a bit silly, but I’ve been doing it automatically for years. I think it’s helped me see the difference between what I believe is the case and what is programmatically guaranteed to be the case, which are very different things that can feel similar if you aren’t careful.

Elegance — December 12, 2023

Elegance

You can argue almost anything with SOLID. In my experience, elegance is more important.

An elegant solution is immediately understandable by everybody. It’s a way of thinking about the problem that becomes _everybody’s_ way, as soon as they see it.

Questions about how we would handle x, y or z have obvious answers – they follow naturally because an elegant way of thinking gives us leverage. 

In general, we should judge our ideas less by how well they conform to some design pattern, and more by how well they bring understanding to our team. By how much leverage they give us to solve our problems, by their simplicity, and – sometimes – by their brevity. To me, that’s elegance.

Collaborate Your Meetings to Death —

Collaborate Your Meetings to Death

Most meetings are only necessary because of gaps in shared context. If we all did everything together, there would be no reason for meetings (except with external stakeholders). But teams can easily fall into a a vicious cycle. Inundated by meetings, people begin to isolate themselves, creating a vacuum in shared context that gets filled by more meetings. 

Break the cycle. Pick a meeting that’s being used to share context and find a way to share it earlier and more continuously.

Doing It Well — December 7, 2023

Doing It Well

If you care about doing it well, you are most of the way there.

That’s no small thing. Doing it well means caring about improvement more than ego. It means being challenged instead of comfortable, humble instead of prideful, open instead of defensive, earnest instead of cynical, and hard-working instead of lazy. It is believing the best way is still out there and going on a mission to find it every day.

If you can find it in yourself, you are very lucky. If you can practice it well enough that others join you, then you can build a great team – and a great team is something very special.

Delivery Today and Tomorrow — May 3, 2022

Delivery Today and Tomorrow

My way of thinking about technical debt is a bit different to what I usually hear 😅 Let’s start by rejecting the usual framing – the idea that we are in some eternal struggle to find the right balance between speed and quality.

We’re not balancing speed and quality.  We’re balancing delivery today against our ability to deliver tomorrow.  We care a lot about delivery today, which may tempt us to compromise on delivery tomorrow. But do you really think that tomorrow, we will care less about delivery? Has that been your experience so far? 

There are cases where delivery today is an existential necessity, but they are rare. Even startups need to deliver consistently on a long enough timeline for technical debt to catch them. But that doesn’t mean we should spend today developing complex architectures, implementing fancy design patterns or factoring new code exhaustively – there are good reasons to create technical debt.

Imagine you are on a trip and have just arrived at your hotel. Your luggage is full of useful items you may need during your stay. Do you assume you will use every item and unpack them all right away? Do you immediately fill cupboards and drawers with your belongings? Of course not! You start by keeping everything together so you know where it is (high cohesion) and unpacking things as you need them (YAGNI).

People give similar advice about building microservices. Don’t try to draw service boundaries at the start because you’ll get them wrong and it will be hard to fix. Keep things together in one service (high cohesion) and extract them only when boundaries become clear (YAGNI).

There are some life lessons here for us!

  • We are usually wrong at the start
  • Arranging is much easier than rearranging

That first point is important. When we start building something, we are probably wrong. Wrong about how our domain works, wrong about how to model it, wrong about which parts of the system will need to be extensible, wrong about our cross-functional requirements – there are so many things to be wrong about! The more heavily we factor our code, the more we pour concrete on our wrong assumptions. The more we arrange things at the start, the more difficult they will be to rearrange later when we find out we were wrong.  

This “we’re probably wrong at the start” thing can be a bit tough for experienced people to accept, as we may have built our professional ego around the idea that we are good at being right. If you think you can get things right the first time, this post isn’t for you.

But if you’re with me so far, then my advice is this. Apply what we’ve learned about luggage and microservices right down to the class and function level. Don’t heavily factor your code at the start – just do enough. To build enough, we can use outside-in, test-driven development, test-driven design and close collaboration to produce the simplest, smallest, least-factored thing that works. Anything we are tempted to do beyond that, we can consider technical debt.

Working this way, our initial solution will be:

  • As simple as possible, because we will produce it outside-in while practicing test-driven development
  • Well structured, because test-driven design will guide us in writing data-driven code, separating concerns and avoiding complex conditional logic
  • Using domain language that makes sense to our business, because we are working closely with non-engineers
  • Sensible to our team, because we are mobbing or pairing with rotation
  • A step in the right direction, because we have thought about the problem holistically (but a small step only, because we have not thought too hard or too long)

The result of all this will be some software and some technical debt, so when and how should we pay that debt off? A few years ago, I would have suggested we briefly record technical debt on a technical debt wall, with axes for effort and value. I’d have suggested reviewing the wall at every story kick-off and picking up items that will make delivery of the coming story easier. That’s not a bad approach – it’s better than what most people are doing.

Now I believe technical debt walls are a local optima. In a team working as we have discussed here, categorising and prioritising technical debt is of little value:

  • Our opinion about how to improve something will change as our skills and knowledge grow
  • Our opinion about what is worth improving will change as we are asked to adjust and extend our software in ways we did not initially predict
  • We may not work in that part of the codebase again in a relevant timeframe anyway

It is more sensible to ask – before making any change to our software – “Can we improve our engineering design *a bit* to make the coming change easier?” Why only “a bit”? Because we don’t want to engage in a wholesale, big-bang redesign based on the need for a single change. Every time we change our software, we want to improve its design in a small increment, so that the kind of change we are trying to make is easier next time. This way our design will gradually evolve to support the kinds of changes we are actually making, in the areas we are actually making them. We don’t need to gamble on what should be extensible, how extensible it should be and in what ways we will need to extend it.

Put everything together and the entire approach is super simple:

  1. Build the simplest thing we can
  2. Every time we change it, make the change easier with a small improvement to our engineering design

With this approach in mind, the dichotomy between delivery and technical debt is exposed as false. We improve our engineering design when we deliver something and when we deliver something, we improve our engineering design. There is no need to categorise work as ”technical debt” or “delivery”. Such categorisation and the careful management that usually follows is a useful local optima for teams that are not evolving their engineering design continuously enough yet, but it shouldn’t be our ultimate goal.

“Technical debt” as a model for thinking about the need to continuously improve our engineering design is a good raft – it can help you get across the river as you learn how to do the right amount of work up-front and master improving it gradually over time. But when you get to the other side of that river, you should put the raft down.

Pressure is a Resource — November 18, 2021

Pressure is a Resource

We talk a lot about the best ways to work, but we don’t talk very much about how to move our organisations toward them.

As a former consultant, I grappled with this every day. Even with a clear idea of some alternative way of working, a large team of people ready to teach it and good executive support, it is still very difficult. One reason for this is that we hastily squander a powerful resource – pressure. 

When a problem impacts our business, organisational pressure is created. We all want to relieve that pressure and that shared desire is a special thing – we can harness it to effect positive change. Unfortunately, many organisations react reflexively and take the path of least resistance. That is how pressure differences work, after all. We look for quick fixes like overworking “temporarily” or juggling more WIP. But quick fixes are not always good fixes.

What if we are only moving the impact somewhere less obvious? E.g. we have started a critical piece of work to placate stakeholders, but the resulting increase in WIP is causing inefficiencies across all work? What if we are only changing the timescale of the impact so that instead of being hurt a little every day, we get hurt a lot all at once? E.g. we are overworking somebody and it will blow up in our face when they quit.

Our quick fixes may actually leave us in a worse place than before. The root problem is still not solved and there is little pressure left to drive improvement. We wasted our chance. 

It’s important that we not reflexively relieve pressure. Pressure helps us grow and change if we channel it wisely. We may need to use some of that pressure to make the immediate impacts of our problems manageable, but to build a great and enduring business we must direct as much of that pressure as we can into lasting solutions to our deepest problems. So spend your pressure wisely and don’t rush relieve it entirely. To improve continuously we need pressure and with no pressure, we can easily get stuck.

If you want a fleshed-out example, you can read my article about adopting trunk-based development. It’s an approach that offers some benefit at each stage, while still leaving or creating enough pressure to drive us toward the next one. 

Focus on Technique —

Focus on Technique

If you want to be a better competitive swimmer, you don’t ask for feedback on your lap times and trophies – you focus on improving your technique and know that results will follow. 

Most software developers are preoccupied with the properties of good code, but knowing what it looks like is not the same as knowing how to write it. A good critic isn’t automatically a good creator. 

How does the length of your red-green-refactor cycle impact your results? Does working outside-in or inside-out change your solution? Do different pairing styles lead to different outcomes? Do you get simpler code by thinking through the problem in advance, or by being deliberately naive? Does writing failing tests instead of passing ones impact your implementation? Do your team members work through problems in different ways and if so, what are the advantages and disadvantages of their approaches?

If you can explain SOLID principles but you can’t answer these questions, forget about *what* for a second and start thinking about *how*. 

Pair with your teammates. Let them show you their way of thinking and working and the next day, ask to show them yours. Set aside time together to reflect on your experience and discuss what you want to do differently next time. Do experiments together and share the results with your team. 

Focus on technique.