I've been working on the comments system for my blog in the past week. Comments systems are complicated; there's not only what the user experiences but there's also entire sub-systems and metadata dedicated to managing comments. Think: moderation systems.
As far as moderation approaches go mine is pretty pragmatic. I have a moderation queue that shows the parent comment, OPs comment, and any replies in a collapsible container. I can leverage that the contextualize the comment for discourse, but I can also preview the GPs comment for further contextualization.
I used the word simple, but what I'm talking about is two sets of APIs: one for users and one for admins (me). Next, I do some database tomfoolery with reverse relationships to display child and parent comments. A preview:
{
"items": [
{
"id": 0,
"author": {
"id": 0,
"username": "string"
},
"content": "string",
"post": {
"id": 0,
"title": "string",
"content": "string",
"created_at": "2024-10-13T18:17:04.363Z",
"updated_at": "2024-10-13T18:17:04.364Z",
"published": "2024-10-13T18:17:04.364Z",
"author_id": 0,
"slug": "string"
},
"children": [
{
"id": 0,
"author": {
"id": 0,
"username": "string"
},
"content": "string",
"post": {
"id": 0,
"title": "string",
"content": "string",
"created_at": "2024-10-13T18:17:04.364Z",
"updated_at": "2024-10-13T18:17:04.364Z",
"published": "2024-10-13T18:17:04.364Z",
"author_id": 0,
"slug": "string"
},
"children": ["string"],
"visible": true,
"created_at": "2024-10-13T18:17:04.364Z",
"updated_at": "2024-10-13T18:17:04.364Z",
"reviewed": true,
"note": "string"
}
],
"parent": {
"id": 0,
"author": {
"id": 0,
"username": "string"
},
"content": "string",
"post": {
"id": 0,
"title": "string",
"content": "string",
"created_at": "2024-10-13T18:17:04.364Z",
"updated_at": "2024-10-13T18:17:04.364Z",
"published": "2024-10-13T18:17:04.364Z",
"author_id": 0,
"slug": "string"
},
"parent": "string",
"visible": true,
"created_at": "2024-10-13T18:17:04.364Z",
"updated_at": "2024-10-13T18:17:04.364Z",
"reviewed": true,
"note": "string"
},
"visible": true,
"created_at": "2024-10-13T18:17:04.364Z",
"updated_at": "2024-10-13T18:17:04.364Z",
"reviewed": true,
"note": "string"
}
],
"count": 0
}
Notice the parent and child relationships have to avoid some circular dependencies, but are optimized for nested relationships. As in all things that are cool I'm using some tricks to produce what looks like a very comprehensive comment system like fetching only top level comments and sorting server side for visibility. All in all, from my tests, even with thousands of comments the system continues to perform pretty quickly.
Now for confession time and the starting in on the overall point of this post: I built most of this on nights and weekends with GitHub Copilot.
AI as a productivity tool
LLMs get shopped as this thing that's going to kill peoples jobs. It reminds me of the discourse around idempotent automation and immutability over a decade ago.
"This one neat trick your sysadmins don't want you to know about will reduce your ops overhead drastically!"
-- Some consultant, probably
The gist is that you didn't need rooms of people managing servers like they were their pets. Instead, we'd manage flocks. What this opened up was a greater focus on the meta of the flock rather than dedicating individual focus to each member of the flock. We knew the flocks size, the cost of each member, and since all members were clones we knew a lot about the characteristics about each member. The philosophy of immutable infrastructure was sold as a lot of things but at its core it was a productivity shift for software operations
Immutable infrastructure was just one philosophy of many that were cannibalized by an overarching movement called "shift left". We, as in industry, focused on making optimal use of the time between when a software engineer puts their fingers on a keyboard and when your change goes live to users. Much of that did indeed require a skills adaptation, but the industry went through many changes including the Great Migration to the Cloud™. Why? Turns out calling an API to spin up infrastructure is just a lot faster and (generally, but not always) less frustrating than filing a ticket for some person that works in a basement.
Fear not though; the basement workers in this story became people like me. I also worked in a NOC, fulfilling tickets and responding to broken things nearly full time; now I work on software through the lens of reliability and developer productivity.
Neither here nor there but... it's funny how some people constantly seek to remove other people from the equation of their business and those people just reinvent themselves.
I wanted to spend some time comparing the present to the past because I feel it's pretty relevant to remind folks we've been here before. Now, back to Copilot. Copilot is a very capable LLM based on ChatGPT's line of products. Of course, it gets things wrong and does dumb things, but if you know how to prompt it then it can be quite capable. That's to say, Copilot isn't even the most capable for what I'm tasking it with; there are newer LLMs that are optimized for generating entire codebases that when put in the hands of a capable, well-seasoned programmer could knock out the most rote tasks of a codebase much quicker than Copilot does for me.
When I use Copilot actively I target it at two things:
- rote tasks that I'd colloquialize as the "busy work" of programming
- highly complex, small surface area problem solving
Number one is pretty self-explanatory to most programmers, but for the non-programmers I'm talking about things like REST API scaffolding. If I've built a user system, RBAC, and logging system then I need to wire each up in every occurance of the CRUD parts of my API. When I build new APIs Copilot goes to work scaffolding that CRUD API and including the components I mentioned. There's some ways to optimize this through prompting and keeping relevant code open in tabs, which Copilot will source from.
One of the examples I like to use is that I needed to write some various date formatting functions
for Typescript on top of Javascript's native
Date
class.
I wanted a function that displays the dates you see in these posts but I wasn't super keen on using
a date library given that the OpenAPI generators I use the native Date class. Copilot came up with
this:
export function formatDate(date: Date | null | undefined): string {
if (!date) {
return ''
}
// change date to the user's local timezone
const offset = new Date().getTimezoneOffset()
const localDate = new Date(date.getTime() - offset * 60000)
const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
const months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
]
const dayOfWeek = daysOfWeek[localDate.getDay()]
const month = months[localDate.getMonth()]
const day = localDate.getDate()
const year = localDate.getFullYear()
const hours = String(localDate.getHours()).padStart(2, '0')
const minutes = String(localDate.getMinutes()).padStart(2, '0')
// Function to get the correct ordinal suffix
function getOrdinalSuffix(day: number): string {
if (day > 3 && day < 21) return 'th' // Handles 11th, 12th, 13th
switch (day % 10) {
case 1:
return 'st'
case 2:
return 'nd'
case 3:
return 'rd'
default:
return 'th'
}
}
const dayWithSuffix = day + getOrdinalSuffix(day)
// store a value with the timezone
const timezone = localDate.getTimezoneOffset() / 60
return `${dayOfWeek}, ${month} the ${dayWithSuffix}, ${year} @ ${hours}:${minutes} (UTC${timezone >= 0 ? '+' : '-'}${timezone})`
}
The comments are largely added by me to guide Copilot into doing the right thing. While some may scoff at this, this is code that I can understand well but on my own would largely never write. In the past I would've reached for a date library and would've handled changing types back and forth.
If I'm starting to sound like a Copilot shill then let me be clear: I don't think this set of constructs is special to Copilot. I've used Anthropic's models to achieve (mostly) the same result in other applications.
Tooling is primacy
As a wood worker I understand a fundamental truth to wood working: tools, and the quality of them, do not guarantee the quality of the product but they can influence the speed to produce a quality product. It's the logic that was at the heart of Industrial Revolution and nearly a century on forward we have not escaped that logic. Company build entire Internal Developer Platforms (IDPs) to speed programmers along to fitting into their tech stack so that they can focus on the business logic that saves or makes the company money. That's to say at some point once a corporation has achieved peak product-to-market fit the difference between it and enterprise becomes efficiency and reliability.
Another analog near and dear to my heart is the story of indie game developers and the explosion of the market we've experienced in the last five years. Platforms like Steam, GOG, and Epic have driven the availability of these games but the tools developers use to build games have also began to find quite a nice middle ground where more developers can do the things they need to produce a good game in a much quicker time than they used to. I'm talking about comprehensive tooling and engine bundles like Godot, Unity, and Unreal engines. There's multitudes of other tooling and engine options I could list here that aim to solve deeper productivity problems than the formers solve like easing the burden to interfacing with multiple languages and other problems that slow programmers down.
For my last analog I present the case that Go and Rust, while being a neater abstraction on top of closer-to-the-metal languages, really achieved most of their adoption and adoration through their bundled toolings capabilties. Go and Rust's cross-compilation, built-in formatters, and package management utilities were tools that developers built affinities for because it reduced the complexity of everything that leads up to packaging software for distribution.
Corporate dominance and its downfall
Largely, mega-corporations (or enterprises) that were built in the early aughts maintain their dominance via acquisition. It's rare that they actually innovate organically, although some like Google are able to maintain some sort of internal innovation cycle alongside acquisitions.
Acquisitions depend on a very expensive process that involves assimilating software, employees, and infrastructure over time - some of which never quite complete or end in disaster. I've gotten to see this process first-hand and I was also around long enough ago to watch companies age and slow down. The slow down companies experience at a certain size relates mostly to the complexity of delivering software on the companies ever-evolving systems intermixed with a complex web of ever-changing demands and needs. That's to say, all things become more complex. Leaders, engineering and otherwise, spend a lot more time forming easy-to-understand social interfaces between teams to build easy-to-understand technical interfaces between those teams. Those relationships require nurturing and maintenance the way that a good friendship does. This all has to happen before a programmer can even think about touching hands to keyboard.
This is all to say that the viability of large scale organizations is threatened by its increasingly large social model that continues to evolve and change under the feat of the people who do the work as well as those who ensure that the work gets done.
My prediction for the future is that companies with a smaller, more nimble and fluid social structure will now have the tools to build increasingly complex products with the tools they have available like the industrial revolutionists, wood workers, and indie game developers before them. In some way, my prediction is a disruption of those who used to consider themselves disruptors of industry.