Perhaps the central issue in software design/architecture is the appropriate handling and constraint of complexity, both up-front and as the software grows. To that end, there are many frameworks, in whatever your chosen language, which have reducing or eliminating complexity at their core purpose. They’ll call it “being opinionated” or “convention over configuration” or some other catchphrase, but they all boil down to, essentially, the notion that taking work and responsibilities away from the developer is the same thing as taking away complexity. This is true, up to a point.
If it weren’t true, no one would ever use any of those frameworks. (Except for, perhaps, Java developers.) The problem is that complexity is a bit like trying to grip a balloon in your fist—squeeze parts of it with your fingers, and other parts will get pushed out wherever there is room. Over the long-term, complexity tends to out, and the harder you try to squeeze it down the more likely it is to explode. By letting the framework lull you into complacency, and ignoring the complexity at the beginning, you’re risking a descent into a lifetime of add-on, callback, and middleware hell to eek out even small improvements in functionality.
This isn’t to say that it is all doom-and-gloom. A good framework can help you get off the ground running. A good developer can use his or her preferred framework judiciously, and can keep themselves from falling into an inescapable black hole of coupling app code ever-more tightly to the framework. A framework is like a giant ox, in that they’re both tools that can really help you out in the short-term, and also really hurt you for the long term, if you don’t approach them with a healthy respect for their dangers as well as their advantages.
People who know my opinions on the matter are often surprised when I tell them that I use ActiveRecord in certain projects. The difference between how I use it and how many large Rails projects use it is that my “models” are a few lines long and consist almost entirely of relationships. I use it as an ORM, and only as an ORM. Any actual business logic is elsewhere, and ActiveRecord is simply a convenient DB querying and data access tool. Because those classes are only used to save and load data—and not, say, to handle the logic of moving money between accounts, or calculating reports, or syncing to Redis, or any number of things that can be crammed into an AR subclass—they’re purely an architectural boundary. On the app side, they’re wrapped by a bridge class with an interface derived from the app and business logic, not from ActiveRecord. As a result, there are zero unit tests that involve AR in any way, the app code can be comprehended and maintained without caring about AR, and systems design decisions like which database storage system is most efficient for different pieces of data can be made almost completely independently of the software engineering.
The tough part, conceptually, is seeing these as good things. Something I’ve heard frequently is “well, we use Rails, so why shouldn’t we use a Rails feature if it’s there?” The “use it because it’s there” impulse is a strong one. This is where the framework making things really easy at the beginning starts to come back to bite: a framework is almost always going to have an easiest-possible solution for something. StackOverflow will have 20 users fighting to be the first to tell you what that solution is, and to claim that their solution is “best practices.” Part of being a software engineer, or a programmer if you prefer, and not a code monkey is not automatically forfeiting those decisions, and instead taking a step back and looking at the effect on your app as a whole. If that simple, “best practices” solution involves turning a 900 line file into a 901 line file, then it’s worth considering what brought things to that point, why that one file does so much, what happens the next time any of that needs to change, and just how confident you are that it actually works as intended.