JUCE Module Design Tips (From Real Projects).
Introduction
Warning: Under Construction
This blog post is mostly note-jotting and brain dumping for now. No timelines are set for its completion.
Real Intro
This post collects a set of design tips and common pitfalls I’ve encountered while writing and maintaining private and public JUCE modules intended for reuse.
None of these are theoretical - they’re the result of real design considerations, experiences like build failures and portability issues, and general integration pain that only show up once a custom JUCE module becomes a dependency.
With great power comes great responsibility - JUCE modules give you a powerful way to package code for reuse, but that power comes with the responsibility of managing APIs, dependencies, and platform-specific behaviour very carefully.
So What’s a JUCE Module, Really?
Surely, after starting your first JUCE project with CMake and/or the Projucer, you’ve seen the JUCE Module Format specs, or at least seen the modules from JUCE itself.
From a conceptual point of view, consider a module as a public API contract where the implementation details are hidden in .cpp/.mm files. Headers define what your consumers can rely on; everything else is internal. Consumers being users as developers; people integrating your module(s) into their app code or, at a higher level, into their own module code.
Formatting & Expectations
Naming conventions are important: filenames, functions, classes, namespaces, the occasional macro… everything should be descriptive, unique, and most importantly - consistent.
This importance goes beyond just names for the sake of names - it’s communication. Consumers should be able find and localise information quickly, in the most digestable way you can concoct: building mental models about your code should be straightforward.
Filenames
First, you should consider naming your files based on their intents: is it capturing a class definition like an Array in juce_Array.h? Or maybe you’re capturing a group of concepts like changing values smoothly in juce_SmoothedValue.h?
With that in mind, I strongly recommend prefixing your filenames with your module name (eg: juce_XYZ.h, squarepine_XYZ.h). This avoids clashing filenames when searching and listing files, and makes it less confusing to read.
As a side note: please stop using plain Utility or Utilities or Helpers as a vague, catch-all bucket file. This is an extraordinarily bad habit and doesn’t help consumers internalise the goal of what code needs to be found in these files. You’re creating a goose-chase that can be avoided by segregating concepts; concepts are searchable.
Let’s make it plain with an in-code comparison:
// Bad: vague, catch-all
#include "Utilities.h"
// Good: concept-specific, prefixed
#include "squarepine_EffectProcessorChain.h"
Using find or CTRL/CMD+F, it becomes abundantly clear where a file lives and which module it originates from (or at least who it originates from) when applying a concept-specific naming approach.
Definitions
- Single responsibility: one cohesive area per module.
- Clear API surface: minimal, boring, platform-agnostic headers.
- Implementation vs interface separation: platform includes and heavy logic in .cpp.
- Predictable build behaviour: no hidden macros or unexpected dependencies.
Transitive Awareness
Exposed headers affect all downstream users. You have to be exceptionally cautious when
Don’t Leak Native or Platform Headers
- Avoid exposing platform-specific headers in module headers.
- Keep native includes in .cpp.
- Platform-specific tips:
- macOS/iOS: .mm files for Objective-C++ segregation.
- Windows:
JUCE_CORE_INCLUDE_COM_SMART_PTR=1in .cpp if COM helpers are needed.
Public Headers ≠ Convenience Headers
- Only include what’s needed for the user to consume the module.
- Minimal headers = reduced coupling, faster compile times, fewer ABI issues.
Be Intentional About Module Dependencies
- Avoid unnecessary JUCE or external module dependencies.
- Use internal helpers instead of pulling in large dependencies unnecessarily.
- Every dependency is a hidden obligation for users.
Avoid “Header-Only by Accident”
- Keep definitions, platform logic, and non-template code in .cpp.
- Inline only truly necessary things.
- Prevent ABI instability and excessive compile times.
Platform Code Belongs Behind a Wall
- Keep
#if JUCE_WINDOWS,#if JUCE_MAC, and any other native things tucked away in the implementation files.- Keep headers neutral.
- Use .mm for Objective-C++ where necessary.
- Benefits: easier maintenance, porting, testing.
Modules Are Long-Term Commitments
- Public API changes are painful.
- Design names and types carefully.
- Treat modules as reusable libraries, not convenient folders.
Unit Test Integration
JUCE UnitTest system has no formal module guidelines.
Provide config macros to enable/disable tests:
#ifndef SQUAREPINE_ENABLE_UNIT_TESTS
#define SQUAREPINE_ENABLE_UNIT_TESTS 0
#endif
Guard all test classes with the macro to avoid polluting downstream test runners.
Insight: Separating module tests from consuming project tests prevents accidental breakage.
Android Integration Gotchas
- JUCE requires unwritten macros and scaffolding for Android:
JNI_CLASS_MEMBERSfor JNI bindings.#define JUCE_CORE_INCLUDE_JNI_HELPERS 1in .cpp before any includes.- Document all macros clearly, keep them in .cpp, and provide usage examples.
- Insight: Explicit scaffolding prevents build errors and builds trust with module consumers.
Final Takeaway
JUCE modules are powerful but sharp: most pain comes from accidental exposure or hidden dependencies.
Keep headers minimal, segregate platform code, manage dependencies deliberately.
Modules like squarepine_core demonstrate careful boundary management.
Following these practices saves build headaches, fragile code, and subtle platform bugs.