Modularity Anti-Patterns
Overview
This document identifies common anti-patterns in modular Salesforce architectures based on real-world refactoring experiences. Each anti-pattern includes the problem, why it occurs, its impact, and proven solutions.
1. The Monolithic Package Anti-Pattern
The Intent Behind Modularity
The whole point of package-based architecture is to enable independent development, testing, and deployment of features. When everything lives in one package, you lose these benefits entirely. Think of it like building a house where every room's electrical system is connected - you can't work on the kitchen wiring without shutting down the entire house.
Problem
A single package grows to contain hundreds of components, mixing multiple domains and responsibilities. What starts as a "core" package becomes a dumping ground for everything.
How It Manifests
src/
└── mega-package/
├── classes/ # 500+ classes: OrderService, CustomerService,
│ # InventoryManager, TaxCalculator, EmailHandler...
├── objects/ # 50+ objects from unrelated domains
├── flows/ # Flows for customer onboarding, order processing,
│ # inventory management, reporting...
└── lwc/ # UI components for every feature in the systemWhy This Is Problematic
Imagine you need to fix a bug in the tax calculation logic. In a monolithic package:
You deploy the entire package (30+ minutes)
All tests run (45+ minutes)
Any team working on ANY feature is blocked
If something breaks, you rollback EVERYTHING
You can't give a client just the tax fix - they get all pending changes
The Real Cost
Solution: Domain-Driven Package Decomposition
The solution is to break apart the monolith based on business capabilities, not technical layers. Each package should represent something a business person would understand.
Now that tax rate change:
Deploys in 2 minutes (just tax-management package)
Only runs tax-related tests
Other teams continue working
Can be deployed to specific orgs
Rollback affects only tax functionality
2. The False Modularity Anti-Pattern
The Intent We're Violating
True modularity means packages can be understood, developed, tested, and deployed in isolation. When packages secretly depend on each other through the database or configuration, they're lying about being modular.
Problem
Packages appear modular but are tightly coupled through hidden dependencies. It's like having "separate" apartments that share plumbing - turn off water in one, and the neighbor's shower stops working.
How It Manifests
Why This Is Dangerous
The code above creates an illusion of modularity. The packages seem independent but:
You can't test PricingService without inventory package's data
Deploying inventory changes can break pricing without warning
There's no contract defining what Category__c values are valid
The dependency is invisible in package manifests
The Hidden Coupling Problem
Solution: Explicit Contracts and Ownership
Make dependencies visible and controlled through interfaces:
3. The Circular Dependency Maze
The Intent of Dependency Management
Dependencies should flow in one direction, typically from higher-level packages (business logic) to lower-level packages (utilities, data access). When packages depend on each other circularly, it's like two people holding doors open for each other - nobody can actually go through.
The Problem Visualized
Real Code That Creates Circles
Why Circular Dependencies Are Deadly
Can't deploy: Package A needs B, B needs C, C needs A... infinite loop
Can't test: Mocking becomes impossible when everything depends on everything
Can't understand: Where does the logic actually live?
Can't refactor: Moving anything breaks everything
Solution: Dependency Inversion and Events
Break the cycle by introducing abstractions and using events for loose coupling:
4. The Chatty Packages Anti-Pattern
The Intent of Service Boundaries
Package interfaces should be coarse-grained - think of them like international phone calls. You wouldn't call another country to ask one word at a time; you'd have a complete conversation. Same with packages.
Problem Illustrated
Why Chattiness Kills Performance
Each cross-package call has overhead:
Parameter marshalling
Service location/injection
Security checks
Logging/monitoring
Error handling
Multiply that by 500 and you have:
Slow performance
Governor limit issues
Difficult debugging
Complex test setup
Solution: Bulk Operations and Aggregated Interfaces
Design interfaces that accept collections and return complete results:
5. The Configuration Coupling Anti-Pattern
The Intent of Package Independence
Each package should own its configuration and not be affected by other packages' configuration changes. Shared configuration is like sharing a toothbrush - it seems convenient until someone changes how they use it.
The Hidden Configuration Problem
Why Shared Configuration Breaks Modularity
No ownership: Who's responsible for EmailFromAddress__c?
Hidden dependencies: Package manifest doesn't show config dependencies
Runtime surprises: Config changes break unrelated packages
Testing nightmare: Tests need specific config states
Solution: Package-Specific Configuration
Each package owns its configuration with clear namespacing:
6. The Overly Generic Package Anti-Pattern
The Intent of Domain Focus
Packages should solve specific problems well, not all problems poorly. It's better to have a sharp knife and a good screwdriver than a dull Swiss Army knife.
The Generic Monster
Why Generic Packages Fail
Impossible to test: Every change requires testing all paths
Impossible to understand: What does this package actually do?
Impossible to maintain: Where do you add new functionality?
Terrible performance: Constant branching and type checking
No clear API: What parameters are valid? Who knows!
Solution: Domain-Specific Packages
Create focused packages with clear purposes:
Key Takeaways
The Business Test
Can you explain what a package does to a non-technical stakeholder?
✅ "This handles tax calculations"
❌ "This processes various operations on multiple object types"
The Deployment Test
Can you deploy a package independently without breaking others?
✅ Deploy tax changes without touching orders
❌ Deploy mega-package and pray nothing breaks
The Team Test
Can two teams work on different packages without conflicts?
✅ Team A on orders, Team B on inventory, no merge conflicts
❌ Everyone fighting over the same files
The Understanding Test
Can a new developer understand a package's purpose in 5 minutes?
✅ "order-management does... order management"
❌ "universal-processor does... everything?"
Conclusion
These anti-patterns emerge naturally as systems grow. The key is to:
Recognize them early through metrics and code reviews
Refactor incrementally - you don't have to fix everything at once
Prevent recurrence through architecture governance
Balance modularity with practicality - some coupling is acceptable
Remember: The goal isn't perfect modularity - it's maintainable, understandable, and deployable systems that teams can work on without stepping on each other's toes.
Last updated
Was this helpful?