One of the features of configuration management platforms, and this is expecially true of Puppet with its resource model, is that it presents an abstraction of specification from implementations. This I can write some Puppet DSL to install a package, that assuming many other things are equal, will work on multiple OS platforms to implement the configuration you expect (installing some software).
There are some higher levels of abstractions available too - putting configurations into Classes, wrapping Classes in Classes to implement things that look like whole application stacks.
One theme that comes up more and more is how to abstract similar but different implementations of a type of service - this might ve necessary because of the heterogeinity of your estate, or for whatever reason the need to have n different ways of providing the same kind of service. One example of this is syslog. EL5 for example used syslog, EL6 moved to rsyslog, and the modern world just loves the syslog-ng. Monitoring presents similar implementation diversity (e.g. Nagios, Icinga, Zabbix etc). So do webservers - Apache/nginx/IIS/whatever.
Taking the syslog example, fundamentally those applications all do the same things, and are implementable and configurable through Puppet code. Given that they do the same things, the modules are likely to accept many of the same parameters, and have similar kinds of configurations. A webserver has a listening port, an idea of content to serve, vhosts. Syslog always has alerting levels married to destinations. There may be some bells and whistles associated with specific implementations of these things, but in general what they provide is an instance of a service.
The tree of the service begins to look then something like:
roleincludes - service::logging - implements one(perhaps more) of - syslog - rsyslog - syslogng - foosyslog
If you accept this level of abstraction from the implementation, you need to worry about only the entry point (in this case service::syslog) in terms of manipulating the lower order classes. That class needs to worry about figuring out which class to include in the catalog. Each functional class should have enough internal wherewithal to be able to implement itself, and be able to have enough configuration data fed into it from the higher level class. I am making the assumption that all these classes will be parameterised and use data bindings/hiera to pull in the data, and then make decisions on which service to implement. It’s possible to do the same thing using an ENC.
The last piece of the puzzle is to think about providing a suitable unified presentation of any defined resources you might use - for example, instances of logging configuration that might be application specific in a thing.dot.d directory. This is likely to be presented via a defined resource in the service::logging module - the defined resource is likely to do the same things as the logging base class - make a decision about which implementation to use, and declare it, passing appropriate data as necessary.
This last piece starts to look like more complexity, and I wonder if the abstraction starts to break down and be a bit too unwieldy for general use.