Yes, agreed, that quote is not as relevant to software development. Some would make the case that software isn't good unless it's perfect. Engineers will use the word 'correct' (e.g. correctness); and by that, they mean; is it perfect... And it's not wrong to demand it because the smallest gap can be a critical security vulnerability.
HN user
socketcluster
Email: jon(at)socketcluster.io
Yes they do. The contract says $X and Y stock options per year, Z hours per week.
I try to find an embarrassingly parallel solution to most problems I encounter. Not only because such solutions scale, but because they often produce a simpler, more robust architecture which helps you to avoid future issues. It's great for avoiding single points of failure and performance chokepoints. Also, there is usually little to no overhead for choosing a parallelizable solution (besides a little bit of additional up-front thinking.)
To people who say "You don't need scalability" I say "You also don't need unscalability..."
It's a terrible metric, it's kind of like companies paying employees by the hour for white collar work.
Most people here probably don't know what it was like to work a contract job and being paid based on actual deliverables.
The incentive of AI companies is to create as many tokens as possible to solve any given problem. Just like your incentive as a software engineer is to create as much complexity as possible in order to use up as many hours as possible.
This is why big tech companies have millions of lines of code... They've got thousands of engineers rapidly churning out tokens.
The difference in number of tokens I use in my day job vs side projects is massive. You can see the inefficiency quantified.
Show me the incentive, I show you the result.
I love vanilla JavaScript. All my personal projects are plain JavaScript. No Typescript. But in a company setting, it's almost impossible to find plain JavaScript roles.
Plain JS is also a lot better with AI. I don't recall Claude ever making any mistakes in terms of getting the type wrong with plain JS since I started using it. It's just not the kind of mistake that AI makes.
I feel somewhat vindicated by this. I've been saying for years that coding isn't the hard part, type correctness isn't the hard part. I also made a point that complex interfaces are a greater danger and that Typescript tends to encourage people to design complex interfaces... And look, this certainly seems to be reflected in the training data because my AI token usage at work (Typescript) is far greater than on my side projects for the same task complexity.
This is kind of proving my point that plain JS code is better architeted overall than Typescript code... It makes sense to me, any complex plain JS code MUST be well architecture because JS is unforgiving. The spaghetti doesn't go very far in JS.
This is neat. I've been dreaming of something like this to host frontends connected to my backend platform https://saasufy.com - I can get Claude Code to create a data-driven app entirely inside an index.html file on my computer's file system, then, because it's built with WebSockets, it doesn't have CORS limitations so I can open it directly from the file system by double-clicking it (served via file:// protocol) then, when I'm happy, I can drag that file and drop it on Cloudflare Drop and then it's deployed online. No text editor/IDE, no server needed in the entire process.
I agree that AI does well when the patterns in the code are predictable and consistent.
That said it can work surprisingly well with custom frameworks and tools provided that they are predictable and consistent.
For example, I created a platform with custom Web Components. Agents do a great job at using the components by reading the docs. I find it a lot easier and more succinct than React. I think it's because AI isn't as good with high level patterns when there are too many pieces involved and too many sub-patterns to apply, it gets so caught up in the details that it misses the forest for the trees.
My SDK abstracts away a lot of low-level complexity so that agents are able to focus on higher-level architectural patterns. Also, it's very succinct so agents can fit a lot of context/functionality into its context window. It gets faster and better as the codebase grows.
Here's the link if anyone wants to try: https://saasufy.com/
ORMs are an anti-pattern. What ends up happening on most projects is that, over time, the ORM ends up generating increasingly complex, inefficient SQL queries behind the scenes. Since some of the people who use the ORM don't understand SQL, they don't realize how inefficient their ORM logic is; it looks like a simple operation from their perspective... It's only if you look under the bonet that you realize that the SQL being generated behind the scenes is a monstrosity. Nobody would have dared write this fugly mass of SQL by hand but from the ORM layer, it looks reasonable... Just a few objects joined by dots....
When building agent integration for my serverless backend https://saasufy.com/, I decided to not use MCP but to put curl commands inside skill markdown files instead: https://github.com/Saasufy/skills
The curl command is extremely popular so models seem to be really good at using it.
Also I like that curl uses a bash syntax and my platform requires JSON payloads; it makes the separation clear to the agent. I find it to be very reliable.
That seems to make the case stronger. It becomes Cloudflare's problem. You can deal with Cloudflare from one country and let them figure out how to collect payment from people all over.
That said, morally, I strongly resent the fact that accepting payment has essentially become illegal for most people due to this complexity and the way globalization has been forced on people. People are essentially not allowed to receive payment to feed themselves. That's what it has come down to. Not everyone can afford an accountant and take that risk.
I think OOP helps to build loosely coupled systems but it doesn't protect you from tight coupling. You have to know what you're doing.
I think OOP languages made some pragmatic decisions. Sometimes a feature which is harmful 95% of the time could be genuinely useful and safe 5% of the time... Some languages like Haskell might choose to not allow that feature at all and force the developer to find another approach which is almost as effective for that 5% of cases; that's fair enough. It's a different philosophy.
I feel like that about passing mutable objects by reference. I find it harmful most of the time but there are rare cases were it's convenient and beneficial. I've worked on open source projects were I wanted the user to be able to use the software with any database so my function accepted a database adapter as an argument.
I could have achieved a similar goal in another way but I would have had to sacrifice separation of concerns slightly. I wanted the ability to substitute any database but also wanted the component to be responsible for the persistence and recovery of its own state as this was within its responsibilities. Also this was the only violation in the entire codebase so I deemed it acceptable. It didn't pose any problems at all in practice.
There are definitely cases where you could have a module with distinct responsibilities; so you can definitely get high cohesion and loose coupling without OOP, that's true, but there are cases where you may want to:
- Control the timing of when a module is activated (instantiated).
- Have multiple instances of a module with variations in functionality where those variations are not a concern to the parent module/instance.
For me, this is when OOP becomes most useful. If I can write some code once and later use it to create any number of independent instances with the same functionality which can clean up after themselves, this is generally a lot more maintainable than having one module to keep track of all the different states in an array and micromanaging (for example) the rendering and cleanup work associated with multiple distinct pieces of state.
IMO, the most important philosophy in all of software engineering is "Separation of responsibilities." The best way to achieve it is through the principle of "High cohesion, loose coupling."
OOP is just another layer of philosophy which builds on top of that. It's more specific, imposes additional guardrails. It requires objects with state encapsulation (locality) and message-passing as the mechanism for components to interact with each other; each component is responsible for changing a subset of the state of the system. Each component is responsible for handling messages (calls to action) by performing local state changes and potentially also sending messages to other components which have more specific sub-responsibilities.
OOP without "High cohesion, loose coupling" is almost worthless IMO. It must build on top.
I think were most people fail with OOP is that they think coming up with good separation of concerns, good abstractions is easy. They just start implementing the first idea which comes out of their heads and then figure out the scope of responsibilities as they go.
The test for good separation of concerns is that you should be able to explain your architecture to someone with the intellect of a 9 year old child who happens to understand the business domain. I'm not exaggerating. It has to be that obvious or else you will not be able to maintain clean separation... You will not be able to maintain alignment in your team.
If the responsibilities of a specific object are somewhat vague, what will happen is that the scope of responsibilities between objects will soon blur and the messages between objects will start to look increasingly elaborate; your system will look like a bunch of horrible incompetent managers trying to micromanage junior employees using long, convoluted instructions and occasionally throwing chairs at them...
If your system is passing around object references all over the place; that's usually a sign of poor separation of concerns; passing around complex objects by reference is tight coupling, by definition. Each object, each person should be able to fulfill their responsibilities and finish the job using communication only.
It supports attribute-based access control (similar to RLS but more granular) and also Group-based access control for more advanced situations.
Authorization is enforced based on CRUD rules that are defined on the control panel on each Model. For Create, Read, Update or Delete actions, the permission can be either "block" (don't allow anyone to perform this action), "restrict" (only allow if the user has a token which matches the resource) or "allow" (anyone can perform this action on the resource without authentication).
Permissions are enforced at the Collection and row/record level by default but can be overridden on a per-field basis so you could, for example, have a record which could be read by anyone but only the owner can edit a specific field. You could also make it so that a specific set of users is allowed to read a resource but only one of them (or perhaps a third one) is allowed to edit.
When the user authenticates themselves, they are issued a signed JWT token.
From the control panel, you just need to specify which property of the JWT to match against which field of the model; it can be the same for all CRUD actions or different for each one. The token contains an accountId property. You just need to select the corresponding field on the model and the backend middleware will match both values to decide whether or not the 'restrict' condition is met. If the accountId in the token does not match the one on a resource, then the user will be blocked. In 'restrict' mode, if fetching a list based on a filtered view, the user will be blocked if the list/page contains a resource which does not match their accountId from their JWT. The views can be parameterized with an accountId field from the client when applying an indexed filter so you can easily define views which meet the restrict criteria for any given user.
You don't really need to know any of this though because you can just ask your AI to define these rules for you and you can ask it to run tests as it can call all the CRUD actions with HTTP and you can make your AI agent impersonate any accountId you want by associating it with the API credential of your AI agent via the control panel.
You can also enforce group-based access control for handling large dynamic groups but the default attribute-based access control is quite versatile and you can optionally have multiple owners on a resource with comma-separated accountIds but you have to update the resources individually. It's good for simple sharing scenarios where one user wants to transfer ownership of a resource to another user. They could add a second owner and then the second owner could later remove the first owner to gain exclusive ownership.
This approach is also useful for simple private chat scenarios where two users are allowed to read a private message but only the sender is allowed to edit it. You can have different properties on the Model to enforce access for read vs update... For example a field readerAccountIds (Read) and senderAccountId (Update).
I've been working on a similar product. Started working on it 14 years ago and pivoted it to vibe coding. https://saasufy.com/
I'm thinking to open source it but I want to see some traction before doing that since I don't want to open source then someone else takes my code and I get nothing out of it.
Because security hasn't been a major concern yet... But wait for the AI models to catch up.
This is why I built https://saasufy.com/ - Vibe coders shouldn't trust themselves with backend security. Unfortunately, it's extremely difficult to get right. There's a lot to think about;
- Schema validation with appropriate size limits on all relevant fields.
- Authentication.
- Access control.
- Backpressure management and rate limiting in case a (possibly malicious) user tries to perform too many computationally expensive actions in a short time.
- Ensuring that the actions of one user doesn't throttle another user which is connected to the same process/host, e.g. using async constructs to avoid freezing the main process.
- DDoS mitigation.
- Avoiding race conditions.
- Designing a good database schema, with well chosen indexes, with deterministic IDs/idempotency to avoid double-insertion scenarios. You don't want to be forced to rely on overly complex queries with a lot of joins. This doesn't scale well and rarely necessary.
- Logging and error handling.
- Avoiding conflicts and accidental overwrite with old data when multiple users are editing different fields of the same resource concurrently.
- Efficient distribution of realtime messages.
- Scalability.
The list goes on and on... And every piece has to be implemented perfectly. This involves a huge number of carefully thought-out decisions.
Wow the token leaderboard idea is nuts. It's similar to trying to measure the productivity of software engineers based on number of lines of code.
Fully agree. Shipping a complete product with a functioning user acquisition funnel is much harder. It's like; you have to build the whole product first with lots of features and then you have to try to create a highly condensed overview of all those features to expose them all on the landing page.
If you can't make the visitor understand your entire complex product in 10 seconds, then you've lost them.
Your product has to be complex because that's where the software market is at. All of the low-hanging fruits have been taken by the time you identify them. Sure, someone will find a way to make money using new low-hanging fruits that arise due to technological changes but it's not going to be you. You probably don't have the business connections to make that work.
Interesting reading this because this is essentially the principle behind https://socketcluster.io/ scalability; the sharding of channels across available brokers is pseudo-random. It uses a hash function for determinism but the distribution appears to be random and that was also the best way I could find to distribute load evenly between available nodes. It is key to its embarrassingly parallel design.
It's interesting to see it being done at the data centre level as well.
I started building something pretty obscure about 14 years ago; https://socketcluster.io/ an open source, WebSocket-based RPC + pub/sub library with a focus on in-order async stream-processing with backpressure monitoring.
It didn't start out like that. Initially, it was just another WebSocket library with a focus on making it easier to scale to multiple processes.
It's kind of mind-bending to me though that it still feels like it's "too early." You'd think that the ability to efficiently process RPCs and pub/sub messages from clients whilst maintaining ordering would be critical... Yet if you look around the industry; callback-based event handlers are still the norm for most application logic and people are still not using queues where they should be. People think of queues as some expensive/bulky system with overhead which requires additional architecture (e.g. RabbitMQ, Kafka, STOMP, NSQ) and always requires exactly-once delivery, they have not tried to make the idea a core part of their application logic. Software today is FULL of race conditions because of this blind-spot. Yet I still cannot communicate my message. It's too difficult to explain the benefits.
Sure. A lot of these things tend to go together. Weird hacks is a bad one. Those AI agents love to cheat and if they see highly elaborate hacks in the code, they won't hold back either.
True. I've worked on projects which required updating 3+ repos for each feature. Required carefully-timed staggered deployments.
It's often a sign of poor separation of concerns. Tight coupling and low cohesion.
On a good codebase with microservices, this should happen on rare occasions, but not every single time you add a new feature. Been there. Agreed those are particularly hard to work with using AI.
It's based on my experience as a software engineer who has worked on both clean and messy codebases with AI.
It's a very different experience with a messy codebase. In this case, the agent spends most of its time trying to gather the relevant context and it's like a game of whac-a-mole. The agent burns through tokens and can take a long time to resolve the issue with a lot of human intervention required. I would say it takes possibly just as long or longer than a human engineer would. Also, psychologically, the temptation for the engineer to trust the AI is massive because they don't want to load themselves up with all that ugly, complex context. They are more likely to let the agent create more hacks on top.
On a relatively well-structured codebase with loose coupling and high cohesion, the experience is usually very positive, mind-blowing, even; because it feels like the agent is reading your mind and fast-forwarding you. You don't need to correct it as much. And when you do, it's usually minor things.
The first case represents a net loss of value because tech debt is being added and compounding the complexity each time a problem is 'solved'. On the other hand, the second case is a significant speedup, for me, I would say it's at least a 5x speedup. I love using AI in this way. I'm in control and not at the mercy of the agent.
It makes me wonder about the state of their codebase if devs needs to consume more than $1500 per month.
It's interesting that AI is finally forcing businesses to think about coding maintenance costs though.
When I started working on https://saasufy.com/ as a dev tool many years ago, I was frustrated that no big company cared about software maintenance costs and I really couldn't imagine a world where maintenance costs would be a problem (which is what my platform was addressing). So this is one positive thing from my perspective, I guess. But how much longer before people put 2-and-2 together and realize that architectural complexity is the leading cause? That's the real moment I'm still waiting for.
Will what's left of the socio-economic system be sufficiently capitalist that I will be able to capitalize on that? That's my next problem.
I've been advocating for this approach for years. It's useful for any kind of data processing. You can't avoid race conditions without using some kind of queueing mechanism and you need backpressure to measure queue capacity. I built this into every aspect of https://socketcluster.io/ - From pub/sub channels, RPCs to event listeners.
This. The core problem is that people assume that all software is necessarily unreliable.
The fact is because they themselves are not capable of producing perfectly reliable software, they assume that everyone else is the same. With this narrow-minded worldview, you would expect software to require constant updates as the maintainer is essentially playing a never-ending game of whac-a-mole.
Not all technologies change. Often, low-level engine APIs are very stable and essentially never change... So why should the software built on top change?
According to OP, the kind of reliable software that we need in the AI slop era would fall in the category of 'dead project'. So they are doomed to create AI slop on top of other AI slop. Good luck to them.
It's interesting reading this.
Preventing these kinds of concurrency issues is exactly why I built https://socketcluster.io years ago. Though it solves the problem at the app layer rather than the storage layer.
But not many developers care about these race conditions it seems.
It's not just an issue with SQL but a more general issue with many programming languages and approaches.
This is a great example because it shows how concurrent executions can lead to significant issues.
A no-code platform packaged as an AI tool for building data-driven applications and serving as a data store for AI to tell it interact with your data; https://saasufy.com/ - Tested with Claude Code and pi.
This is why I built https://saasufy.com/ - There are 23 generic HTML components which can be assembled to provide a flexible way to render any kind of data and flexible form elements to flexibly update the data (or show errors when validation fails). It's fully declarative so there is very little room for errors. I find that this helps a lot when working with LLMs. There are no complex bugs. The only kinds of bugs you might encounter are syntax or UX related. No weird race conditions or complex technical issues.