Why I Didn't Use Drupal Commerce for a Simple Donation Feature
When tasked with adding a "Buy me a coffee" button or building a donation landing page on Drupal 10/11, the natural reflex for many developers is to immediately run: composer require drupal/commerce.
Drupal Commerce is a phenomenal engine. But in a recent project, I decided to pause, step back, and write a custom module called Simple Stripe Donation instead. Here are the architectural decisions behind that choice.
1. Domain Problem vs. Domain Model
The core issue isn't that Drupal Commerce "slows down requests" or is inherently "heavy." The issue is that Drupal Commerce introduces a domain model and ecosystem vastly larger than the domain problem we are trying to solve.
If the domain of the problem is simply:
Donation → Payment → Confirmation
Then dragging in a massive abstraction framework that includes:
Store → Order → Order Item → Cart → Customer Profile → Payment Gateway → Payment
creates textbook accidental complexity.
Maintaining this massive data structure for a simple, one-time payment increases long-term maintenance costs, database overhead, and testing surface area.
2. Right Tool for the Right Domain
This is not an argument that "Drupal Commerce is bad." On the contrary, it shines when you actually have "Commerce" requirements. Architectural decision-making always boils down to choosing the right tool for the right domain:
| Use Case | Appropriate Solution |
|---|---|
| Full-scale E-commerce | Drupal Commerce |
| Product catalog & Inventory | Drupal Commerce |
| Cart + Order fulfillment | Drupal Commerce |
| Subscriptions / Complex Routing | Drupal Commerce (or equivalent) |
| One-time donation | Simple Stripe Donation |
| "Buy me a coffee" | Simple Stripe Donation |
3. Principle: Let Stripe own the payment UI; let Drupal own the business integration
Historically, we often tried to render credit card inputs directly on our websites to "keep the user on the page." But doing so means adopting severe PCI DSS compliance risks and the burden of building responsive UIs for a constantly growing list of payment methods (Apple Pay, Google Pay, localized wallets).
With a Stripe Hosted Checkout integration architecture, I completely shed that responsibility:
Drupal (Create Checkout Session)
│
▼
Stripe Checkout (User submits payment)
│
▼
Stripe Server (Process & trigger Webhook)
│
▼
Drupal (Handle Webhook)
I don't need to own the payment UI. Sensitive card data never touches my application server. The Drupal backend's only job now is singular: execute the integration flawlessly.
4. Webhook Architecture: The Heart of the System
In any asynchronous payment system, webhook handling is where architectural competence truly shows. You can never implicitly trust incoming payloads without verification.
The webhook flow in my module is strictly designed as follows:
Stripe Webhook
│
▼
Verify Signature (Prevent payload tampering)
│
▼
Extract Event ID / Payment Intent ID
│
▼
Check Existing Record (Database lookup)
│
┌────┴────┐
│ │
Exists Not exists
│ │
200 Process
│
▼
Persist State (DB Transaction / Unique Constraints)
│
▼
Dispatch Event (Allow other modules to hook in)
To permanently solve the Idempotency problem (Stripe may retry webhooks multiple times due to network lag), I didn't rely solely on application-level PHP checks. I enforced it at the database schema level using Unique Constraints on the session_id column. Furthermore, wrapping the database persistence in a DB Transaction guarantees that no partial states or dangling records ever occur.
5. Conclusion
Sometimes, writing a new piece of code isn't about making a system do more. It's about preventing the system from carrying the weight of things that don't belong in its domain.
⚙️ Implementation: To see exactly how these concepts of Webhook verification, Idempotency, and Event Dispatching are implemented in code, check out the source code and the practical case study: Simple Stripe Donation: A Lean Payment Solution for Drupal 10 & 11.