I have tried just writing end-to-end tests. I have tried writing unit tests after the fact. I have tried writing unit tests before the fact (TDD?). They all lead to problems.
I would like to lean towards a new model, that I would call "contractor-driven engineering" (CDD). The idea is to engineer software systems like a contractor builds a building. First you get a dumb person, who has the basic skills of a trade (so, your average software developer today). But then you bolster their naivete and inexperience with known quantities and an approved architectural design. The end result is a person with a basic level of skill, that can still be relied upon to create a reliable system.
This requires two things first, though:
1) The "known quantities" would be your well-defined, pre-produced, quantifiable engineering parts. Like how a 10d and 16d nail are built to a specification, so that when you architect a building frame, you know that the 10d nail will work in this one part, and the 16d nail will work in this other part, and you can be assured the building will not fall down under a strong wind. We do not have known quantities built to an engineering specification in the software world. We have software "things" that "an experienced person" says "should work", but if we built skyscrapers like that, everything'd be fucking falling over all the time (the way it falls over in the software world all the time, but nobody minds, until all of Delta's flights around the world get cancelled....).
2) The "approved architectural design" - we need people who are actually trained and certified as architects, to dictate the design to the software development team. I think in my 20-year career I have seen perhaps one team actually build software the right way (and by "the right way" I mean "not falling into obvious traps that anyone with enough experience has seen and would avoid"). That architectural training needs to involve the codification of a lifetime of engineering discipline (that is available, if you ask us old fucks for advice), combined with the "known quantities" above, so that designs actually meet stated goals and requirements. This, by the way, completely jives with the idea of "changing software all the time", because you can just ask the architect to look at the change and tell you if it'll work or not.
Why would I think this is better than TDD? Because TDD still assumes the developer is going to simply test their way out of bad decisions. You can't do that. But what you can do, is abstract away the decisions that are hard, and leave the work that is predictable and not hard.