Skip to main content

Why I Didn't Use Drupal Commerce for a Simple Donation Feature

Submitted by Anonymous (not verified) on

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.

Image
Simple Stripe Donation
Image
Simple Stripe Donation

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.

Architectural decision record: When to use Drupal Commerce and why a one-time donation problem requires a much leaner domain model.

Simple Stripe Donation: A Lean Payment Solution for Drupal 10 & 11

Submitted by Anonymous (not verified) on
Simple Stripe Donation: A Lean Payment Solution for Drupal 10 & 11

When deploying personal blogs, NGO websites, or community platforms on Drupal, the requirement to accept credit card donations is incredibly common. However, the default reflex within the Drupal ecosystem is often to install the Drupal Commerce suite.

While Drupal Commerce is an outstanding e-commerce framework, if the use case is simply "a reader wants to donate $5 to support the author," dragging in the entire commerce ecosystem represents a massive architectural and maintenance trade-off.

  • Domain Complexity: Introducing abstractions like Store, Order, Order Item, Cart, and Customer Profile creates far more domain complexity than the use case actually requires.
  • PCI DSS Compliance: Self-hosting a credit card input form on the website demands rigorous security standards and compliance overhead.
  • Asynchronous Reliability: Asynchronous payment transactions are prone to duplicate states without a secure Webhook handling mechanism and strict idempotency.

Design a completely standalone module, decoupling the logic entirely from carts and orders:

  • Offload Payment UI: Utilizing Stripe-hosted Checkout significantly reduces the Drupal application's PCI DSS compliance scope, as card information is entered and processed entirely on Stripe's infrastructure.
  • Event-driven & Idempotency: Building a dedicated webhook endpoint to handle asynchronous flows from Stripe, guaranteeing data integrity.
  • Drupal Native Integration: Leveraging Drupal Core's Block API and Form API to provide a flexible UI for administrators.

 

Instead of manually managing the transaction state on the frontend UI, the module acts purely as an integration layer between the user and Stripe.

[ User on Drupal Site ]
      │
      ▼ Synchronous flow
[ Drupal Controller / Service ]
      │ (Create Checkout Session)
      ▼
[ Stripe Hosted Checkout ]
      │ (Payment Processing)
      ▼
[ Stripe Server ]
      │
      │ Asynchronous flow (Webhook)
      ▼
[ Drupal Webhook Endpoint ]
      │
      ├── Signature Verification
      ├── Idempotency Check
      ├── Persist Donation State
      └── Dispatch Event (DonationSuccessEvent)

This architecture clearly illustrates the separation of concerns: Drupal handles internal business logic, Stripe manages the payment UI and card security, and the two communicate via reliable Webhooks.

  • System Streamlining: Completely eliminated the dependency on Drupal Commerce for one-time donation use cases.
  • Security First: Card information is never processed or stored on the Drupal application; the payment lifecycle happens entirely on Stripe Checkout.
  • Open Source: Released under the GPL-2.0-or-later license, ready to be dropped into any Drupal 10/11 project.

Let Stripe own the payment UI; let Drupal own the business integration. Not attempting to own complex, high-risk domains (like credit card processing) keeps the codebase lightweight, secure, and highly reliable.

Design for evolution: Dispatching a custom event after successfully saving a transaction allows the module to be easily extended later (e.g., triggering emails, logging to a CRM) without modifying the core logic.

Technical Deep Dive: I wrote a dedicated architectural note on why I chose to build a standalone donation module instead of utilizing Drupal Commerce. Read more here: Why I Skipped Drupal Commerce for a Simple Donation Feature.

Subscribe to Drupal