Hacker News new | past | comments | ask | show | jobs | submit login
Software design gets worse before it gets better (tidyfirst.substack.com)
233 points by KentBeck 13 days ago | hide | past | favorite | 140 comments





To boil it down simple:

People are vaguely good and competent, they leave systems in a locally-optimal state.

In general only changes that are "one step" are considered, and they allways leave things worse when you are currently in a locally optimal state.

A multi-step solution will require a stop in a lower-energy state on the way to a better one.

Monotonic-only improvement is the path to getting trapped. Take chances, make mistakes, and get messy.


I think "better" is ambiguous.

Better for developers? Better for users?

Better for speed? Better for maintenance? Better license? Better software stack? Better telemetry? Better revenues through subscriptions?


I would disagree with that. We have quality measures to assess what 'better' can mean in software engineering (e.g. maintainability, reliability, security, performance, etc.). You are right that it is not fixed what is most important. It may vary from one organization to another, but it can be conceptualized and then made measurable to some degree.

At a business, all of these - a good engineer/architect has to find the right balance.

Different engineers can have different interpretations of "right balance" -- and many of them may be correct. Which makes "better" again ambiguous.

I think this is actually addressed in the article:

> The key question for the designer is, “What would the system’s structure need to be so that <some feature> would be no harder to implement than necessary?” (It’s a bit surprising when designers don’t ask this question, instead simply asking, “What should the design look like?”—for what purpose?)

During my career, I have been in many situations where the SW architects tried to answer the second question: as if the architectural cleanliness was the goal unto itself. Software design patterns were misused, unneeded abstractions abounded everywhere, class hierarchies were created 15+ levels deep. There it was often brought up which is better and nicer and cleaner because the metric was aestetics.

Most of those arguments, however, are quickly brought to a stop, if we are actually asking the first question: how hard is it to add these new features? That said, I was frequently unable to convince coworkers in my past employments, that aestetics of the design is not the goal. They simply clung to it, to somewhat religious extent, identifying themselves with their "artwork".


I have concluded that smaller is better and straightforward is better. I think it’s easier to scale up a small system than to maintain a complex system that was built for scale from the ground up but usually got some things wrong because the requirements at the time weren’t clear.

But in the end there is never a clear answer. I am happy when people can explain what the positives and also the drawbacks of a design are. Pointing at “best practice” without explaining pros and cons is usually a big red flag.


Indeed, could not agree more. Also, using composition over inheritance is age old advice but that still did not prevent those architectural astronauts from creating inheritance structures 15+ levels deep. Luckily, newer languages make constructing such monstrosities harder and discouraged.

"Luckily, newer languages make constructing such monstrosities harder and discouraged."

They just encourage constructing monstrosities of a different kind. I don't think people who do stupid things in one paradigm will do any better in another paradigm. I see that a lot in the microservice vs monolith debate. If you can't manage a monolith you will also screw up microservices.


Just having a huge bikeshedding festival at work after I wrote this sentence into guidelines...

The worst thing is how hard it is to talk about it because all the books - by authors making more money from talking about software than from writing and maintaining code - recommend it, and it's just so SOLID and Hexagonal and looks obviously intuitively correct.


Can you elaborate, you have piqued my interest :)

The sentence for preferring "composition over inheritance" for code reuse is in the book by the gang of four (the design patterns book). I really don't understand how we ended up in the situation, where 30 years old advice is still valid and still not followed. I lay, perhaps too much, blame on Java, which seems to have this baked into its infrastructure, but similar approaches have also been adopted in C++ with multiple inheritance making things even worse.

I mean, SOLID, when used appropriately, is also valid. The problem is that the design patterns are used where a simpler solution would work just as well.


This is exactly the problem - the advice is valid, but the developers can't see their implementation is not, and my examples of maintainable code seemingly go against the advices.

They think their huge class diagrams and statically unverifiable mess of many structurally identical classes (in Typescript) is an example of DRY, composition, separation of concerns, inversion of control and encapsulation - all the great advice neatly packed into 50 files opaquely interconnected through dependency injection containers, where a simple 100 line function would have done the job and wouldn't cause a major headache for the poor guy who has to fix a bug in 3 years.

The root issue is that these guys never were the poor guy who has to fix a bug after 3 years. They moved on after a year or two of "implementing best practice approaches" to the next job.


> "What would the system’s structure need to be so that <some feature> would be no harder to implement than necessary?"

This sounds good, but in my direct experience it is really really hard.

For example, sometimes you have a feature that is really easy to add. Just add a new argument or keyword or command and implement it in the guts.

But every once in a while you get a beautiful architecture that has a "direction" to it. And a horrendous requirement comes along and breaks everything. For example, port it to macos. Or add and call this third-party library. Or break it up into an SDK, a CLI and a web service.

sigh. guess that's why this kind of career keeps you on your feet.


Ambiguous only in the sense that it doesn’t always mean exactly the same. The direction of better on a graph is always in the same direction though, even if the point isn’t at the same x,y coordinate.

It's not ambiguous, it's a collective decision between engineers and business stakeholders. The ambiguousness comes from engineers not having full information.

I find the biggest issues in industry and organizations is so called “tech debt” and no plan for future improvement of a solution as it matures or user base scales. Planning for these is essential.

I dont think users care about sofwtare design.

At most they might care about non funcional requirements (e.g. security and performance)


Which is why facebook had the motto "move fast and break things", you need to break bad abstractions to get to good abstractions and solve problems.

I decided i would prefer to quote Ms Frizzle instead of this.

Unfortunately it happens often enough that you manage to sell the step where you make things worse, but then you never get management buy-in for the step that makes things better again.

TL;DR you can't make an omelette without breaking eggs?

You can always make it with blood instead.

LOL

> Monotonic-only improvement is the path to getting trapped. Take chances, make mistakes, and get messy.

The evolution disagrees.


Evolution is a satisficer not optimizer.

All major trophic level breakthroughs are powered by evolving a reserve of efficiency in which mult-step searching can occur.

multicellular life, collaboration between species, mutualism, social behavior, communication, society, civilization, language and cognition are all breakthroughs that permitted new feature spaces of exploration that required non-locally-optimal transitions by the involved systems FIRST to enable them.

Trust is expensive and can only get bought in the presence of a surplus of utility vs requirements.


Wow, very well put. Any suggestions for academic papers, books, or even online resources on these topics would be greatly appreciated.

This is related, and it is the paper that lives constantly rent free in my head. I think it will retroactively be viewed as revolutionary: https://www.alexwg.org/publications/PhysRevLett_110-168702.p...

Basically, intelligent behavior is optimizing for "future asymptotic entropy" vs maximizing any immediate value. How intelligent a system is then become a measure of how far in the future it can model and optimize entropy effectively for.

(updated with pdf link)


Great paper! There are some similar ideas to this in game theory and reinforcement learning (RL):

[1]: Thermodynamic Game Theory: https://adamilab.msu.edu/wp-content/uploads/AdamiHintze2018....

[2]: piKL - KL-regularized RL: https://arxiv.org/abs/2112.07544

[3]: Soft-Actor Critic - Entropy-regularized RL: https://arxiv.org/abs/1801.01290

[4]: "Soft" (Boltzmann) Q-learning = Entropy-regularized policy gradients: https://arxiv.org/abs/1704.06440



I didn't say that evolution finds the optimal state, just wanted to highlight how far it was able to go, much farther it seems.. (like evolution of the eye)

But your comment was refreshing, could you briefly expand on the "multicellular" life part? Did you mean that it enabled more non-locally-optimal transitions, or that it required them to appear?


I think cooperation is never a locally optimal strategy. Somebody allways gets to pick second at the prisoner's dilemma table, and locally optimal behavior is to eat the trusting idiot.

Takes a lot of luck to evolve cooperation multiple times at once, much more likely to happen in a situation where the selection pressure is lower, not higher.


Now you get into the definition of "locally". Gene pool local or individual local? I think it's evident that cooperation has proven highly effective at the gene pool level. Will it prove to only be effective short-term local and flame out over longer-time spans remains to be seen. Will there be anyone to document it? Not sure, but it's been a helluva ride.

Pretty sure those non-cooperative strategies quickly burn themselves to extinction though. The selection pressure itself would be regulated towards an equilibrium.

The thing about evolution is that you are sampling many times in different directions. So "luck" isn't that hard to achieve.


> Pretty sure those non-cooperative strategies quickly burn themselves to extinction though.

> Pretty sure those non-cooperative strategies quickly burn themselves to extinction though.

Um, most life hanging out in the same tropic level or lower is heavily predated upon. Competition is the norm.

Luck is hard for cooperation because it is a coordination problem. You basically have to evolve cooperation entirely as a unexpressed trait then trigger it in the population almost simultaneously. The mechanisms of cell cooperation are critical dividers on our evolutionary trees for a reason, they are rare and dramatic in consequence. Cell populations regressing in terms of coordination behavior (see cancer) is one of their most problematic failure modes and it is only very weakly selected against.


>Um, most life hanging out in the same tropic level or lower is heavily predated upon. Competition is the norm.

I'm referring to the predator-prey population cycles. If you overexploit your prey you are going to run out of food and see your population thin out rapidly from starvation. Hence hyper-competitive strategies would get outbreeded by less competitive but sustainable strategies.

High predation levels would require equally high cooperation levels amongst prey to ensure rapid reproduction to sustain the food supply. If we go down the food chain it's the same thing, plant life, celluar life, etc, has to be flourishing to sustain the upper levels.


You quoted this part,

> Take chances, make mistakes, and get messy.

But then seemed to indicate evolution disagrees.

I might be misunderstanding your point, but it sure seems like, evolution tries a bunch of stuff, and whatever reproduces kinda wins.

That seems like, take chances, make mistakes, get messy. That seems like the core of evolution.

Could you clarify or refine what you’re saying? The two seem at odds.


So that was a bad quote, I only wanted to address the part that mentioned monotonic-only improvement, since to me, evolution has achieved more than I'd imagine, evolving organs like the eye incrementally.

I got inspired by this article: https://writings.stephenwolfram.com/2024/05/why-does-biologi...


Basically, the root disagreement was "monotonic improvement". Evolution is awesome, but it couldn't work with only monotonic improvement.

I used to do an "optimization" on my genetic algorithms. I'd ensure the highest scoring genome of the last population was a member of the new one. It made sure every single generation improved or stood still.

It was a good idea to keep a copy of the "best" genome around for final output, but by keeping it in the search space, I was damaging the ability of the algorithm to do it's job by dragging the search space constantly back to the most recent local optima.


Evolution regularly ends up in local optima that it struggles to get out from. Species go extinct all the time when there's no evolutionary path that solves its problems.

And on the flip side, a sufficient abundance of resources and/or lack of predators mean non-optimal species can procreate, and thus find other local optima.

In terms of evolution, the fitness of a species is defined by its ability to reproduce. In the circumstances you describe, selection pressure exists for the species that can reproduce the fastest. Predators or resource constraints are not a requirement for evolution.

> In the circumstances you describe, selection pressure exists for the species that can reproduce the fastest.

My point was there's no pressure without constraints. A faster-reproducing species will only apply a pressure if starts exhausting a resource or similar.


I didn't mean to say that evolution avoids local optima, but I wantend to say that it doesn't have to get "trapped", in the sense that it was able to produce such complex organisms as humans..

Because of the incremental nature (it can't think moves ahead) only a tiny percentage of all viable life forms can be produced by evolutionary processes. Species forged by their ruthless struggle for survival. Maybe humanity will be the first species to escape evolutionary constraints, but maybe humanity is like the many other species that burn brightly but briefly. The universe seems to be cold and empty and devoid of life. Perhaps evolution is very good at producing cockroaches and not that good at creating intelligent life.

Right, but it could well be that there is some other greatly superior organization of life toward which there exists no evolutionary path.

What makes you think we're not trapped in some really mediocre local optima? Tree dust makes me sick.

>> Monotonic-only improvement is the path to getting trapped. Take chances, make mistakes, and get messy.

> The evolution disagrees.

It’s not an either/or. Vast modularized localized improvement allows for the ability to prune and select what does and doesn’t work.

https://hbr.org/2020/01/taming-complexity


<gif="blinks in Cambrian Explosion"/>

> The evolution disagrees.

Ah yes monotonic-only improvement by way of making every small, messy mistake possible and still probably going extinct is definitely the way to go


The multi-dimensional nature of this problem makes it extremely fascinating to me, even twenty years into my career.

There's a certain dopamine hit you get for voluntarily trudging into the trough of despair, pushing the Sisphyean boulder of better design uphill in the optimistic belief that you can do better, and then actually arriving at something slightly better.

Way more fun than conceptualizing programming as duct-taping libraries, frameworks, and best practices together.


Only if people wouldn’t switch jobs every 2 years.

Only if new joiners wouldn’t feel like they have to “show up with something” making existing stuff obsolete.

Well not blaming people or companies just thinking out loud.


Only if companies compensated fairly instead of incentivizing 2 year job hopping.

Only if companies treated workers with dignity and not like they’re disposable cogs.

Only if companies understood value of standards that would prevent new joiner from wreaking havoc.


I've gotten two raises in my entire tech career for actually staying at a company, and at one of them I was laid off six months later anyway. At this point any company that doesn't want me to eventually job hop will need to have an advancement structure laid out from day 1.

A good dev I worked with was promoted to senior, due to a vacancy. They were an improvement on the decent abilities of the person who left.

The CTO confided in me one day, around six months to a year after the promotion, that the dev deserved a fat salary raise because they were doing well with their new responsibilities -- but the CTO was worried that promoted dev would expect that kind of pay raise again in the future, when the organization clearly wouldn't be able to do it.

I called the CTO dumb and told them the promoted dev was doing well at their job for the same reasons that they'll understand that fat pay raises can be a one time thing.

Whenever I think of people leaving because they aren't getting pay bumps, I don't think of managers being stingy. I think of really weird mis-expectations and what must've happened in the past to build that expectation in managers' minds.


I think about this mismatch quite often and the pay issue is sometimes debated well on HN. Mostly it is people complaining about fair wages like /r/antiwork.

I wish we had a balanced discussion from both sides (Company founder/owners and employees). The issue is complex and outliers are often used as status quo.


The market rate is set by the competition, who may or may not choose to follow a historic trend.

The CTO was more likely being dishonest rather than stupid.

Most justifications for not giving a pay raise are carefully crafted bullshit designed to sound reasonable.

After all, if the reason is "I want more money available for me and the shareholders" you can't just say it.


If the company has gotten to the point that the only way to provide returns to the shareholders is to squeeze employee salaries, take that as a sign that the company can’t increase revenue and is struggling, and it’s time to look for another job.

(If, for some reason, the company is making gobs of money for the shareholders but management has still decided to squeeze employee salaries, increasing the risk of loosing key people that help generate that revenue, also leave because those managers will end up destroying the company.)


Is it really impossible for an individual engineer to make decisions that are good for business, and still feel inclined to job-hop?

This kind of stuff can happen in open source projects too

So there is a pretty obvious analogy in chemistry: activation energy.

https://en.wikipedia.org/wiki/Activation_energy

The ELI5 version is that atoms are all trying to find a comfy place to be. Typically, they make some friends and hang out together, which makes them very comfy, and we call the group of friend-atoms a molecule. Sometimes there are groups of friendly atoms that would be even comfier if they swapped a few friends around, but losing friends and making new friends can be scary and seem like it won't be comfy, so it takes a bit of a push to convince the atoms to do it. That push is precisely activation energy, and the rearrangement won't happen without it (modulo quantum tunneling but this is the ELI5 version.)

In the software world, everyone is trying to make "good" software. Just like atoms in molecules, our ideas and systems form bonds with other ideas and systems where those bonds seem beneficial. But sometimes we realize there are better arrangements that weren't obvious at the outset, so we have to break apart the groupings that formed originally. That act of breakage and reforming takes energy, and is messy, and is exactly what this author is writing about.


What is "better software" though?

On one hand you have guys like the OpenBSD team that work on Mostly Boring Things and making serious inroads at improving software quality of Mostly Boring Components that power the hidden bits of the Internet that go relatively unnoticed.

On the other hand, you have "improvements" from Apple and everyone else that involve an ever-changing shell game of moving around UI widgets perpetuated by UI designers on hallucinogens.

Are these browsers like Chrome that are elaborate ad dispensing machines really improvements from the browsers of yore? IE 4 may have sucked by modern standards but it also didn't forward every URL I visit to Google.

I've been around since the beginnings of the WWW and it's reached the point where I am struggling to understand how to navigate these software "improvements". For the first time I have felt like my elderly parents using technology. I haven't gotten stupider; the software has become more difficult to use. It has now become some sort of abstract art rather than a tool for technologists.


First paragraph: “better at supporting new features.”

Further down, he talks about changing the structure of the software in order to support planned features, etc.

So putting it all together, “better” == more featureful at lower cost with reduced marginal pain (to the developers) of further expansion.

I’d say “better” should mean enabling users to achieve their goals with minimal friction for the user (i.e., program p is designed to allow users do task (or set of tasks) t faster/better/more efficiently/whatever). But of course I would say that, I’m a user of software, not a developer of it.

Consider the notion of a Mac-assed apps. They make life as a Mac user much nicer because they integrate so well with the environment and other native apps. But lo! L unto man was revealed his Lord and Savior Electron. Much nicer for developers than having to port programs across several different native environments. So native goes the way of the dinosaur (with some exceptions, of course). That’s a massively canned just-so story, of course, so don’t take it too seriously as actual analysis.

But the moral of the story is that, as a user, it’s endlessly fascinating to me, watching developers talk about development and how much their focus tends towards making their lives as developers easier, even at the cost of sacrificing users’ experiences and expectations as guiding principles.

Love him or hate him, but it’s one of the things that I appreciate Linus Torvalds for emphasizing occasionally: computers are tools people use in order to get things done (for whatever purposes, including recreation).

(That said: There is an irreducibly human element of play involved here for developers too. And even non-developers can be fascinated by computers in/for themselves, not just as sheer tools you’d ideally not even notice (in the Heideggerian sense of tools ready at hand versus present at hand). I’m one of those outsiders. No shame in it.)


Bad code is hard to read. Good code is easy to change.

That's it, I think. Then you recurse up into architecture.

Bad architecture is hard to follow. (spaghetti code) Good architecture is easy to change.

Yes, this means you can have code that's neither bad (it's easy to read) nor good (but still hard to change). In the past I've called this "lasagna code" - the layers are super clear, and it's easy to follow how each connects, but they're far too integrated to allow for any changes.

It's harder to phrase on the level of "software", but mabye something like:

Bad software is hard to use. Good software does its job and then gets out of the way.


> I've been around since the beginnings of the WWW

Ditto.

> I haven't gotten stupider; the software has become more difficult to use.

I can't speak for you, but I'm becoming less interested in new shiny in a lot of things beyond UI widgets. There's a reason why we olds have a reputation of falling behind, and it's not because engineers and inventors explicitly make things that only young people can learn.


it's not because engineers and inventors explicitly make things that only young people can learn

Are you sure? Replace "young" with "inexperienced" and that's exactly what I see in most new software: the focus is on the broadest userbase possible, which is entry-level products and UIs. Nobody's focusing on making expert tooling, everything is geared towards the lowest common denominator -- because supposedly that's where the money is.


At first glance I didn’t like this article (due to a long history of poorly executed redesigns for design’s sake) so I gave it a few minutes and reread it, and now I like it.

Sometimes when reviewing people’s redesigns, I can’t see the beautiful thing that they’re envisioning, only the trough. And over the years I’ve noticed that a lot of redesigns never make it out of the trough. I like the idea of doing small things quickly, I think that’s good, but that’s also technical debt if the redesign never results in a benefit.


You can build stairs on the near side of the trough before you commit to climbing down into it.

Prototyping and figuring out where the most friction is, chipping away at it with each new feature that touches that area.

One of the cleverest things I figured out on my own rather than stealing from others, was to draw the current architecture, the ideal one, and the compromise based on the limits of the our resources and consequences of earlier decisions. This is what we would implement if we had a magic wand. This is what we can implement right now.

It’s easier to figure out how to write the next steps without climbing into a local optimum if you know where the top of the mountain is. Nothing sucks like trying to fix old problems and painting yourself into new corners. If the original plan is flawed it’s better to fix it by moving closer to the ideal design than running in the opposite direction.

What usually happens is people present an ideal design, get dickered down by curmudgeons or reality, and start chopping up their proposal to fit the possible. Then the original plan exists only in their heads and nobody else can help along the way, or later on.


> Sometimes when reviewing people’s redesigns, I can’t see the beautiful thing that they’re envisioning, only the trough.

Distinguishing between the idea and the implementation is vital.

If the idea is good then a few rounds of review is all that's needed to shore it up. If the idea is bad, then there's more work to be done. Letting people know that you like the idea is key. There's also room for being okay with the implementation if it differs from how you'd do it.


Metaphors get abused in this article in a confusing way, and I don't think it explains why the quality curve goes downward at first -- the initial drop in quality is compared to an initial capital investment? what? -- but I agree with the truth of it.

I think the article could be a lot shorter and easier to understand if it simply said that the current design is in a local maximum, and you have to work your way incrementally out of the local maximum to reach a different local maximum. I think programmers would get that metaphor a lot more easily than the "buying widgets for a new factory" metaphor.

I do like how the article puts the spotlight on designing the process of change: picking the route, picking the size of the steps, and picking the right spot to announce as the goal. That gives me a lot of food for thought about the changes my team is contemplating right now.


Wonderful summary.

Perhaps to rephrase it even simpler:

To reach higher mountains we need to climb down our current peak, walk through valleys, until we find higher mountains to climb.


What a wonderfully constructive comment. This is a great model for me to remember when I encounter things I like the substance of but dislike some of the specifics. Thank you!

Appealing to a local maximum more or less repeats the claim.

Why would the current design be at a local maximum in the first place?


because otherwise you'd improve it incrementally?

Yeah, it has a good point to make (change means abandoning local maxima), but overplays its hand.

The curve picture feels like a false idol, as soon as he starts doing TA on it, the carriage is well in front of the horse



I wonder how this translates to today's microservice craze that is more of a infra/devops/org decision that leaks into the software design in various imo detrimental ways. I can certainly see scenarios where merging microservices could unclog the pipes immensely - but I guess that could be construed as a rewrite.

Infra, devops, and (software engineering) org decisions ARE part of software design. Application layer is not the only consideration worth prioritizing design time over.

Microservices is just a buzz word for an overly prescriptive (thankfully waning in popularity) type of distributed system. When you are developing a distributed system, the infrastructure is a primary consideration that is potentially even more important than anything in the app layer.


Sure. But the field looked a lot different in 2000 than today, and the weight of each of these have shifted vastly along with different hypes, trends, the introduction of cloud providers etc.

Microservices in particular is often decided at such an early stage and on such loose ground that in many cases it can barely be called an intentional software design, but rather something more akin to picking a perceived one-size-fits-all template. But it does then certainly leak into everything else - completely unnecessary or not. Which is why I'm asking that question.


Kent Beck is not talking about rewriting from scratch. He's explaining how to transform software to a better state by taking tiny steps in changing the existing system.

I suppose you could interpret Joel's essay as a bit of an agreement with Kent's take. That even though you might have to pass through a trough of despair to rework some old code into something better - that's still a better path to follow than a ground-up rewrite.

I'd usually agree, especially as things get big.

But Kent is also pretty famous for throwing out code if things aren't shaping up. He does this in micro increments however, usually with just-written code.

I've just spent years wrestling with someone else's poorly written, ill-intentioned code, bringing it into line. I've taken the above approach of slowly reworking it. Sometimes I wonder if I just kept the tests and jettisoned large bits of it if I'd be better off?

Very contextual of course, but sometimes you have to explore a little bit to know the right places to make tradeoffs.


> I've just spent years wrestling with someone else's poorly written, ill-intentioned code, bringing it into line. I've taken the above approach of slowly reworking it. Sometimes I wonder if I just kept the tests and jettisoned large bits of it if I'd be better off?

That's the main reason we have this discussion in the first place IMO. There is no one right answer to the question.


Which is why I find folks that throw out Joel's article to be a little black and white. Like "never rewrite anything from scratch".

It takes many years to develop good intuition around this stuff though, so I appreciate that as a first approximation. It can get a little dogmatic amongst senior folks though.


When it comes to a whole application rewrite, I'm pretty certain you should at least be much closer to "never ever rewrite" than "in some cases rewriting makes sense"

If you are talking about refactoring or changing/replacing parts of a system - that's not the same thing. At least to me


>I've just spent years wrestling with someone else's poorly written, ill-intentioned code, bringing it into line. I've taken the above approach of slowly reworking it. Sometimes I wonder if I just kept the tests and jettisoned large bits of it if I'd be better off?

I find it really depends on the level of nuance required of the final behavior. Maybe the test suite doesn't cover certain implicit requirements of the software, often a bug becomes a feature without anybody noticing in sufficiently old projects.

Likewise the tests might not even be structured in a way that's conducive to a rewrite, depending on their level of specificity. Maybe you only care about the final, black box behavior and individual unit tests should be thrown out so you don't need to adhere to existing function I/O requirements.

It just depends. Like you said, very contextual.


I've made a career out of bashing half-baked code into shape, or dragging legacy code to meet new needs. You have tests? You can be much more confident about where you're going. Usually the first thing I do when dealing with code is write some tests to capture how it behaves now, before making any changes.

That sounds familiar :) Keep in mind the existing tests are typically half-baked too, so there's lots of additional testing going on no matter what.

Sometimes I just reflect on YEARS mucking with code written from someone that was just learning, and I wonder if I'm a little too in the "sunk cost investment" mindset. Hard to tell I suppose, but certainly worth thinking about.


> Sometimes I wonder if I just kept the tests and jettisoned large bits of it if I'd be better off?

Depends how much you trust your test coverage.


It seems no one here quite knows what he is talking about.

Then you read his latest book "Tidy First" and it tells you when you move out multiplying width and height into an area function you have now made a beneficial design change in your system and the relationship between caller and box, a "tiny step". And suddenly all the doubts wash away.

Not sure what it is with this industry, but the writing is just useless.


Yes. And Joel Spolsky had a complementary thesis. Written quite well.

In context that's a pretty funny article to see how it didn't survive.

Netscape pre 5 or 6 was a mess. It was a downloadable desktop application that kept getting pushed to deliver new features with a struggling UI framework. Additionally, I would imagine that the group delivering this was rather small in respect to the size of the task. They didn't have CI/CD, git, etc to give feedback. This reeks of an overmainaged project that was intentionally underfunded.

Ultimately.. it was an unmaintable mess that required a rewrite to even continue. To me it sounds like it was tech debt piled deeper and higher.

What came of this? Complete browser rebuilds (mozilla mosaic, chrome, etc), and finally this caught fire through the Chrome project and Javascript acceleration at google.


To me this article is super valid for most software projects.

As for the Netscape anecdote, I wouldn't put too much weight on that part.

We do not know the extent of it, we do not know if it achieved its goals, and we absolutely can not say whether or not the alleged rewrite contributed to or affected the evolution of the product into firefox and eventually chrome etc


I would go as far as to say the rewrite and aggressive reconcepting of Netscape spurred the growth of newer browsers. NS 6 added too much that people didn't want or didn't want in that context.

My comment above was trying to point out: NS6 rewrite wasn't the only browser to start back from scratch at that time.

What I think Spolksy was advocating for: Don't try to completely rewrite things for fun, there are a lot of dark corners there.


good design and implementation requires skilled people. you don't get either with bottom of the barrel pay grades.

something I have noticed in this industry is that big companies think they can outsource their staffing issues and "save on labor". But in the end they pay more in management of outsourced assets, inevitable maintenance of poorly designed and implemented software, delays in delivery, and of course the churn and burn of hiring/firing contractors. Then they end up redoing everything with local talent with 1/8th the team in half the time.

It only took 3-4 years to realize this but this is what the "trough of despair" really looks like.


It's beautiful that this sort of expediency often comes back to bite decision makers. Unfortunately, the timescale in which it occurs makes it very possible to simply ignore the fact that they created the problem in the first place.

This also is why I do not believe LLMs pose as big a threat to software development as we're told. Maintenance will always require humans that can simultaneously comprehend the system as it is today and the system as it should be in the future.


You don't necessarily get them at high pay grades, either. I know people making fat salaries who truly can't manage to write anything decent, it's all a big JS monstrosity with 500 MB of broken dependencies and six build tools that all jump major versions every eight weeks.

Salary has long since been disconnected from skill, ever since cheap money flooded the industry, and easier abstractions made it seem like "everyone can code". Perhaps "fog a mirror" shouldn't be the only programmer criterion.


I don't understand that this article at all or what it means by worse. It clearly defines what better means: that the architecture is such that implementing a feature is no harder than it absolutely has to be. So if it has to get worse first that means initially we're making it harder to implement the desired features? Why are we doing that? Are we counting a half-done, under-construction state? It's harder to implement a feature now than before because we partially wrecked the old architecture, and then you architecture is not done yet? Or is it because we're accounting all this new re-architecting work towards the cost of the first new feature? The first toilet install is hard because we have to do the whole plumbing in the building, and redo the sewage pipe out to the street? The second toilet is easy?

I don't think it holds up to much scrutiny. I think it's mixing up "less improvement than expected for the effort" with "actually measurably worse".

That’s the lesson we learned: implement the most simplistic thing first with just a bit of basic principles like separation of concerns. Humans are terrible at predicting where a system will expand in the future. Therefore just stay out of your own way by not over building!

I mainly agree although I think that the trough of despair often comes after an initial bump. At first when designing the new system, you pluck the low hanging fruit of improvement for a small subset of the system. There is no dip yet -- things are just getting better. But when you start migrating the rest of the system, you inevitably do hit that dip and descend into the trough of despair before climbing back out.

The art is to design things in such a way so that a minimum amount of time is spent in the trough.


Interesting discussion … it appears that the nonlinear nature of modifying a software by a dev team with incomplete tacit knowledge of the underlying design makes it inevitable that things would end up in a state of ruin: small changes become very costly and risky, etc.

What so often happens is you make a plan like this, then business priorities change/things took longer than expected/people leave or join and then you wish you never started...

The absolute highlight of my work is when I get a new projected 'scaffolded' (bad choice of words, but I was using it before it became a buzzword)

You know when you get to the point your data structures just make working on the code a breeze, when your library functions provide all the right low pieces to whip up new features quickly and easily with names and functionality that actually fit the domain... Basically, when all the pieces 'gel' :-D

That for me is programming nirvana :-D


The same applies to tech debt: https://jeremymikkola.com/posts/2022_01_29_tech_debt_gets_wo...

(Yes there's a typo in the url. It bugs me, too)

prior discussion: https://news.ycombinator.com/item?id=30128627


My experience is the exact opposite. To implement a new feature, I usually first refactor, make space for the new feature, improve existing design. (uphill). Then I implement the new feature as pristine and clear as possible (top). Then I face the reality, integration tests fail, I add edge cases I forgot, (downhill). And I end up at the bottom, ready to push that abomination to git and forget about it.

As one of my commits said:

    * Replaced dusty old bugs with shiny new bugs.

Not my experience at all. My original design decisions rarely change fundamentally. And whatever small changes I decide to implement, I implement step-by-step with each step being a refactored improvement.

It probably helps that I have 30+ years of experience and always pick architectures I have used before on successful projects.


Firstly, Hey Kent, how have you been?

Secondly: I think this may be reflective of someone that hasn't sat down and realized the environment that they're in. Creating a poor architecture or approach for the first go is usually a sign of dysfunction or inexperience.

Inexperience: It's more that the individual hasn't sat down, realized that the initial approaches are in appropriate and should be designing first before pushing forward. Experience should be fleshing out a lot of these details before coding anything and get the protocols and conflicts resolved months before they happen. (This is where I see a Staff+ being responsible and assisting in the development of the project)

Dysfunctional environment: Our culture in software engineering has forgone formal design before creating a solution. Typically, most development is dictated by "creating a microservice" first and then trying to design as you go along. This is so aggressive in a coding first approach that many even forgo testing. Why does this exist? Partly the incentives by business/management to deliver fast and distrust in the survivibility in the product.

---

That being said: Am I promoting a "perfect design" (as I've been accused of doing) first? No, iteration will happen. Requirements will change, but if you're applying strong tooling and good coding practices.. rearranging your arch shouldn't be as big of an issue as it currently is.


I’d be interested in the tail end of the graph. I assume that the longer the software is in operation, the more complex and worse it gets. From an anecdotal perspective, that’s my experience anyway having worked on some legacy projects in my time.

Genuine question, is it a property of software design only? Think about construction, for any change in the architecture, one has to demolish stuff and make a mess. I'd argue, that's a general property of change.

Absolutely. There's a gap between code which expresses/communicates its intentions and code which achieves the same goals while being much more streamlined and suited to the constructs of the language.

Needed this today. I think sometimes engineers go crazy and go try to greenfield something, anything, because building stuff requires it being in a nonfunctional state for a sec and this is hard enough on its own but there being (understandable, but often very counterproductive) friction around that that comes from what you're working on being something someone is relying on can make it a really daunting and frustrating process due to the inevitability of the trough

It's not a good idea to rewrite working code from scratch. But I've found starting a greenfield project next door to working code often works well.

Not every new feature needs to go in an existing repository. Sometimes it makes perfect sense to implement the new functionality in a separate executable and artifact that doesn't carry along all the technical debt of the old project.


Needed this post and this comment. And hopefully I'm not misreading either. Halfway through my greenfield redesign that I claim is catharsis because I was too scared of breaking things before they got better. I hope that I can put that redesign to rest and actually make progress with the original code.

Often when trying to improve a complex system, the best way to gain new insights and approaches is to just make a branch or even a clean project and try to redo a thing. Even if it doesn't end up in the project often you'll get ideas and inspiration


I wouldn’t refer to the intermediate states as a software design. The “trough” is the transition from an old design to a new design. But it’s not a software design itself, and therefore not a worse design. It’s just the fact that if you can’t go from the old to the new design in one go (a typical situation), then you’ll have an intermediate phase where the code base is in a more inconsistent and/or complex state, and therefore objectively worse if it were to stay in that interim state. But it’s not the software design that gets worse before it gets better, unless you’re doing some design exploration (hopefully not in production).

I have been wondering who could write such a wise article title before I found out it's Kent Beck himself ...

I always think of projects as something you constantly iterate on, rewriting in my experience has always been a mistake.

A product is something you can iterate on. A project is a unit of work, which can be towards a product, but usually has some kind of end condition.

This matches my software development process:

https://gavinhoward.com/2022/10/technical-debt-costs-more-th...

Not everyone can do this, however.


Why does that make me think about x11->wayland core?

Because Wayland is worse than X11. Unfortunately, it is true that sometimes after your software design gets worse it gets better, but other times after it gets worse it gets worse. It's not correct to assume that something will get better just because it is currently worse.

Wayland went for minmalism because one can always add things later. Unfortunately, making additions in a multi-desktop environment turned out to be kinda difficult.

In a way, the quality of the design (and implementation) is fine, but the quantity of features is insufficient and limited by social issues.

It's basically the opposite problem of systemd.


Wayland is not trying to do enough. You can send pixels to a compositor - great. It turns out a desktop is more than just pixels sent to a compositor. It turns out X11 has a lot of stuff because there is a lot of stuff to do. It could be simplified somewhat, producing incompatible X12, but not simply thrown away along with all the accumulated knowledge. It's literally the Joel Spolsky "never rewrite Netscape" effect in action. Wayland has now spent nearly a decade reaccumulating a part of the same knowledge that was already known, but worse.

wayland annoys the toxic people, because it is currently favoring easy alternative implementations.

It won't last, probably.

Those toxic people usually always get their way into "core", pouring tons and tons of interfaces and dirty everything, viciously adding "features" to provoke some level of planned obsolescences over some few years cycles.

Pure evil.


I'm not sure who you are being sarcastic about in the end, but yes, Wayland is indeed clean and simple enough to not particularly discourage alternative implementations.

Huh.

Indeed, we agree.

We just need to do the hardest part: keep it clean and simple to not particularly discourage alternative implementations... on the long run.


For example, it's vitally important that wayland resists adding features like title bars, window icons, resize handles, desktop icons, or mouse cursors. These frivolous features have no place in a serious desktop environment! They are only bloat!

...while also doing what users want and expect.

... while avoiding their tantrums which will most likely destroy that "clean and simple".

"tantrums" is when you expect windows to be delineated with borders apparently. You got the Wayland attitude down pat: are we the problem? No, it's the users who are wrong!

This does not concern wayland, this is a user choice, the choice of a wayland compositor.

It seems the other way around: x11 got worse and worse, then wayland happened (wayland core).

In what way did X11 get worse and worse? It's one of the most stable protocols in use - outliving HTTP, IIRC. It didn't change - our expectations did. Sure, there are incremental improvements to be made, and incremental improvements that have been made, all of which are optional and therefore couldn't have made it worse.

x11 is absurdely gigantic and dirty compared to core wayland.

We can implement an alternative of a real-life wayland compositor reasonably. A real-life alternative xserver+window manager is not on the same order of magnitude in terms of work.

That reason alone is enough, but if you dig a bit deeper you'll find more reasons to drop x11: you don't need external libs, code is static, x11 bazillion of libs is an abomination of ABIs, much better emphasis on interfaces being optional, etc. For us, wayland is not planned obsolescence, we mean core wayland from a few years ago. We am expecting sabotage by toxic people pouring tons of interfaces in "core" (like they are doing for vulkan3D sabotage... and what they did for x11).

We am still running native x11, only because we run the steam client and only because of that (like 32bits support code).

We are expected to write our own wayland compositor (linux dmabuf, plain and simple C99), then we will have to suffer xwayland for the sake of that horrible steam client, because we have a sin: we play native (#noproton) elf/linux games.


Sure, X11 was getting long in the tooth and not well fit for the modern desktop.

However, the main problem is that the replacement isn't a replacement. If X11 was a pickup truck, Wayland is a bare engine and transmission package.

Sure it might be a much better engine with cleaner design and has no trouble passing emissions testing, and the transmission has much better efficiency and smoother gear shifts. But if you need a pickup truck then it's just one, albeit important, piece.

By leaving all that extra work up to the individual compositors only reinforces the negative aspects of open source fragmentation.


Huh? You don't need libs to use X11. You can just open a socket and send messages. And thanks to the protocol actually defining most of the message IDs you don't even need as much nonsense. 60% of that "bloat" in X11 is stuff Wayland actually needs, and lacks, making it a failure until it gets them. 75% of the rest isn't harming anyone, besides the trees the extra documentation is printed on.

Have you implemented a Wayland compositor by yourself? Without libs?

Desktop systems having desktop system calls is not "sabotage" any more than Linux having open and close syscalls is "sabotage". At some point you have to draw lines in the sand, and say which stuff is in scope and which stuff is out of scope. A useful API does things, which means there have to things in scope. You can't leave everything as an optional extension unless you are defining some kind of absolutely generic message transport layer with absolutely no semantics (not even message ordering or reliability). Is Wayland a protocol for making GUIs appear on the screen, or a protocol for launching nuclear missiles? Right, so it should have features that are relevant to making GUIs appear on the screen. These are core features. "Make a GUI appear on the screen" is a core feature of Wayland even if you pretend it isn't. By requiring the use of five extensions to do that, you do not make anything simpler or more modular, because they are interdependent. You do not increase compatibility, because now you have effective compatibility profiles based on extension sets that are equivalent to protocol version numbers except you didn't call them that. You just make the compatibility story more complicated for no benefit. Abdication of responsibility for a thing doesn't make that thing unnecessary.


Kent said some things, drew some kumbaja diagrams...

This also applies to iOS:

    iOS 6 = good
    iOS 7 = bad
    iOS 8 = better

And now:

    iOS 16 = good
    iOS 17 = bad
    iOS 18 = even worse (and ugly)
    iOS 19 = hopefully good



Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: