I don't know about the "most effective", but I find the following helpful.
Abstraction is key, and comes in multiple dimensions and sizes. Abstractions should be useful and complete and allow the consumer to define and implement another abstraction in domain terms that it requires.
Technical debt means poor abstractions; abstractions that are hard to use, and, abstractions that are incomplete so as to necessitate the consuming client making up needed differences, and that by doing so ends up understanding and relying upon more of the underlying implementation than desirable, which at best results in tighter coupling and and at worst, in spaghetti and/or soup!
In increasing order of size, an abstraction can be: a single constant, a single method, a set of methods, an interface and/or class, a package of interfaces and/or classes, a layer.
Layering is about creating vertical abstractions that rest and build upon each other. When we create an abstraction that provides exactly the domain terms the next programmer up needs (to create their layer), it is effective at isolation. A layer is a set of interface and/or classes that provides a complete set of domain-oriented entities, relationships, and behaviors. Layering allows separation of concerns so that a middle layer can be redesigned without disturbing higher layers.
Creating and maintaining layering as code evolves requires constant awareness of the consuming client's domain requirements, recognition of a layer consumer's difficulties due to mismatch in domain requirements, and regular refactoring to create and keep layers useful and complete.
In the large, layering allows us to switch technologies, such as implementation languages.
We also need to be able to identify when we are crossing responsibility boundaries, such as organizational, departmental, or business boundaries. Responsibility boundaries merit special attention. We design appropriate domain-oriented abstractions at those boundaries. At boundaries we define horizontal abstractions that allow various organizations of both humans and automated systems to assemble themselves into a larger ecosystem.
At some point, as our endeavors increase in size and scope, we need to pop up from the code alone so we can speak effectively of our vertical layers and horizontal boundaries. This is architecture or architectural abstraction. Architecture helps us connect the dots between our requirements & intentions and design choices & implementations.
Centralization fights separation of concerns, which in turn fights good abstraction.
A single shared database for all persistence can be attractive for a small sized endeavor, but co-location of persistence makes it very easy for applications having different ownership and responsibilities to directly access each other's information instead of going thru an appropriate horizontal boundary.
A single shared rules engine similarly collects business logic from what are logically separate concerns and creates inter-dependencies of those rules that are difficult to tease apart later.