After almost a decade at 8th Light, I still find myself amazed at how often I can go back to find more wisdom in the fundamentals. Early in our careers—when we're focused so much on getting better at doing "the job," trying to learn several things at once—these fundamentals can become unconscious knowledge, and it's useful to take time to re-examine the basics with a more experienced perspective—one that has (at least somewhat) mastered mechanics.
Going all the way back to what I consider the "atom" of how we architect or design programs, we get a tricky word to define: abstraction. Like many terms in our field, it's loaded with varying definitions, used in different contexts. So what exactly is an abstraction, and what makes it abstract?
I like to think of abstraction, outside of code, as a sort of conceptual shorthand, or a shortcut to simplify a complex process and distill it into a simple, more conversational description of cause and effect. The abstractness, or what I hear called level, of the abstraction is often related to the level of detail that shortcut is able to gloss over. An abstraction that involves specifics or granular detail is "lower-level," or more concrete. An abstraction that is generic or hides lots of detail is "high-level," or more abstract.
Note that these terms are fully relative. That's because there is always more detail.
As an example, I could describe a car by saying "an engine turns a driveshaft, which connects to an axle, which turns a wheel, which pushes the car forward." The mechanics of how the engine turns the driveshaft, for example, aren't really described here, so this would be a relatively abstract description of how the systems of a car work.
To get more concrete, I could say that "fuel and air are ignited in a cylinder, which drives a piston, which mechanically turns a driveshaft." To get even more concrete, I could say that gasoline is sprayed into the cylinder by a fuel-injector, and so-on, and so-on. Again, there's always more detail!
These layers of abstraction are common and useful mechanisms for thinking about complex systems. You can focus on the big and fuzzy picture, or the smaller, more detailed picture—but very few people are able to think about all the content in the big picture with 100% detail at all times. It can be helpful, if we're fixing our car's brakes, to not have to worry about how the rest of the car works.
Further, this can often align with just good engineering sense. A wheel is just a wheel, so it doesn't really give a darn if the engine pushing it forward is electric or gas-powered. If we allow ourselves to think about the relatively common maintenance use case of changing a worn-out tire, we don't want our end-user to have to disassemble the engine to do so.
In code, an abstraction is a named function, or a module, or a class, or a namespace. Note that these concepts don't have anything to do with OO or FP or anything in between, just about how we think about the cogs in the machinery.
Making the right abstractions in your system, and finding how specific or generic they are, is an art form that requires experience and iteration to perfect. It takes practice and multiple tries, even within a single project or codebase. With this in mind, setting up our systems to respond to iterative change via test-driven development and loose coupling becomes a big part of success.
And here, we find another reason that revisiting the basics is so essential. So much of our knowledge is intertwined and interdependent like the example above—good testing harnesses can lead to good abstractions by providing a test-bed for iteration and feedback, and good abstractions can enable better testing. It might seem like circular logic at times, but upon deeper reflection it's really a positive feedback loop, where two tools or practices make each other better.
Of course, that's just an abstract characterization of how it works. The details are up to you.