Choosing the lightest change that works
The ladder of change
We treat customization as a ladder and climb only as high as the request requires. The bottom rung is configuration: Moodle has a very deep settings tree, and capabilities can be allowed or prohibited per role in any context. Next come data extensions, meaning profile fields and course custom fields. Then presentation, which belongs in a theme. Then behavior, which belongs in a plugin. Each rung costs more to build and to maintain than the one below it, so a request that can be met with a role override should not become a plugin.
Why core edits cost so much
An edit inside Moodle's own files works on the day it is made. The bill arrives later. Updates overwrite the file or conflict with it, security fixes get delayed because nobody dares to deploy, and the knowledge of what was changed leaves with the person who changed it. When we inherit such a site, the first job is a file-by-file comparison against the matching official release. Most hacks turn out to be replaceable. A hidden menu item is a capability or a theme override, a changed email is a language string, and an extra check on enrolment is an event observer in a local plugin.
Presentation changes live in the theme
Moodle renders pages through output renderers and Mustache templates, and a theme may override either. That lets us reshape the course index, the activity header or the login page while the underlying code stays stock. The risk is that an overridden template drifts from the original when Moodle changes it, so we override the smallest template that does the job and keep a note of which release it was copied from. Larger visual work is covered on our Moodle theme development page.
Behavior changes live in plugins
When a rule has to run by itself, such as notifying a manager before a certificate expires or enrolling someone into the follow-up course on completion, we write a local plugin. It listens to Moodle events, runs scheduled tasks, adds its own settings page and defines capabilities so you control who can use it. Larger features get the full treatment described under Moodle plugin development.
Language strings are an underrated tool
The language customization tool stores your wording separately from the language pack, so updates do not undo it. Renaming a few dozen strings often removes more support tickets than a new feature would. We keep the changed strings in your repository, so they can be reviewed and moved between staging and production like code.