The trick to programming Puppet is to remember first that it is not a typical imperial style language, it is a programmatic case logic block style. A good example is that at the end of each command, you must call notify to the next block that you’d like to run.
An Excellent Paper on Puppet by Dan Bode (PDF)
All the magic of Puppet comes down to Types and Providers, developed in Ruby they are what really makes the plain case logic of Puppet come alive:
Types
The resource types provide the model for what you can do; they define what parameters are present, handle input validation, and they determine what features a provider can (or should) provide.
PuppetLabs – Guide to Creating Custom Types
Providers
The core of Puppet’s cross-platform support is via Resource Providers, which are essentially back-ends that implement support for a specific implementation of a given resource type. For instance, there are more than 20 package providers, including providers for package formats like dpkg and rpm along with high-level package managers like apt and yum. A provider’s main job is to wrap client-side tools, usually by just calling out to those tools with the right information.