Magento_Checkout Anti-Patterns
Magento_Checkout Anti-Patterns
Magento_Checkout Anti-Patterns
Overview
This document catalogs common mistakes, bad practices, and anti-patterns when customizing the Magento_Checkout module. Each anti-pattern includes the problematic approach, why it's wrong, the correct solution, and real-world consequences.
Target Version: Magento 2.4.7+ (Adobe Commerce & Open Source)
Anti-Pattern 1: Modifying Core Checkout Files
The Wrong Way
// app/code/Magento/Checkout/Model/PaymentInformationManagement.php (MODIFIED CORE FILE)
namespace Magento\Checkout\Model;
class PaymentInformationManagement implements PaymentInformationManagementInterface
{
public function savePaymentInformationAndPlaceOrder(...): int
{
// Custom validation added directly to core file
if ($this->customValidator->validate($cartId)) {
throw new LocalizedException(__('Custom validation failed'));
}
// Original code continues...
}
}
Why This Is Wrong
- Upgrade Path Broken - Core file modifications are overwritten during upgrades
- Merge Conflicts - Manual merging required for every Magento update
- No Traceability - Difficult to track what was changed and why
- Testing Issues - Can't distinguish between core bugs and custom changes
- Multiple Developers - Conflicts when multiple devs modify same files
The Correct Way
Use a plugin (interceptor) to extend functionality:
namespace Vendor\Module\Plugin;
use Magento\Checkout\Api\PaymentInformationManagementInterface;
use Magento\Quote\Api\Data\PaymentInterface;
use Magento\Quote\Api\Data\AddressInterface;
use Magento\Framework\Exception\LocalizedException;
class PaymentInformationManagementExtend
{
public function __construct(
private readonly \Vendor\Module\Service\CustomValidator $customValidator
) {}
/**
* Validate before placing order
*
* @param PaymentInformationManagementInterface $subject
* @param int $cartId
* @param PaymentInterface $paymentMethod
* @param AddressInterface|null $billingAddress
* @throws LocalizedException
*/
public function beforeSavePaymentInformationAndPlaceOrder(
PaymentInformationManagementInterface $subject,
int $cartId,
PaymentInterface $paymentMethod,
?AddressInterface $billingAddress = null
): void {
if (!$this->customValidator->validate($cartId)) {
throw new LocalizedException(__('Custom validation failed'));
}
}
}
Register in di.xml:
<type name="Magento\Checkout\Api\PaymentInformationManagementInterface">
<plugin name="vendor_module_payment_validation"
type="Vendor\Module\Plugin\PaymentInformationManagementExtend"
sortOrder="10"/>
</type>
Real-World Impact
Case Study: A merchant modified Magento\Checkout\Model\PaymentInformationManagement to add custom fraud checks. When upgrading from 2.4.5 to 2.4.6, the file was overwritten, disabling fraud protection for 3 days until discovered. Result: $45,000 in fraudulent orders processed.
Anti-Pattern 2: Blocking Checkout Operations in Observers
The Wrong Way
namespace Vendor\Module\Observer;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
class SlowExternalApiCall implements ObserverInterface
{
public function __construct(
private readonly \Vendor\Module\Service\ExternalApi $externalApi
) {}
/**
* Call external API synchronously during checkout
*/
public function execute(Observer $observer): void
{
$order = $observer->getEvent()->getOrder();
// BLOCKING CALL - Takes 3-5 seconds
$response = $this->externalApi->syncOrderToWarehouse($order);
if (!$response->isSuccess()) {
throw new \Exception('Failed to sync to warehouse');
}
}
}
Registration:
<event name="sales_order_place_after">
<observer name="vendor_module_sync_warehouse"
instance="Vendor\Module\Observer\SlowExternalApiCall"/>
</event>
Why This Is Wrong
- Checkout Performance - Adds 3-5 seconds to every order placement
- Timeout Risk - External API downtime causes checkout failures
- User Experience - Customer sees loading spinner for extended period
- Scalability - Checkout throughput limited by external API speed
- Reliability - Single point of failure for order placement
The Correct Way
Use message queues for asynchronous processing:
namespace Vendor\Module\Observer;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
class QueueWarehouseSync implements ObserverInterface
{
public function __construct(
private readonly \Magento\Framework\MessageQueue\PublisherInterface $publisher
) {}
/**
* Queue warehouse sync for async processing
*/
public function execute(Observer $observer): void
{
$order = $observer->getEvent()->getOrder();
// Queue for async processing - returns immediately
$this->publisher->publish(
'vendor.warehouse.sync',
json_encode([
'order_id' => $order->getId(),
'increment_id' => $order->getIncrementId()
])
);
}
}
Consumer (communication.xml):
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/consumer.xsd">
<consumer name="vendor.warehouse.sync"
queue="vendor.warehouse.sync"
connection="amqp"
consumerInstance="Magento\Framework\MessageQueue\Consumer"
handler="Vendor\Module\Model\Consumer\WarehouseSync::process"/>
</config>
Consumer Implementation:
namespace Vendor\Module\Model\Consumer;
class WarehouseSync
{
public function __construct(
private readonly \Vendor\Module\Service\ExternalApi $externalApi,
private readonly \Magento\Sales\Api\OrderRepositoryInterface $orderRepository,
private readonly \Psr\Log\LoggerInterface $logger
) {}
/**
* Process warehouse sync
*
* @param string $message
*/
public function process(string $message): void
{
try {
$data = json_decode($message, true);
$order = $this->orderRepository->get($data['order_id']);
// Now safe to make slow external call
$response = $this->externalApi->syncOrderToWarehouse($order);
if (!$response->isSuccess()) {
throw new \Exception('Warehouse sync failed: ' . $response->getError());
}
$this->logger->info('Order synced to warehouse', [
'order_id' => $order->getId()
]);
} catch (\Exception $e) {
$this->logger->error('Warehouse sync failed', [
'message' => $message,
'error' => $e->getMessage()
]);
// Re-throw to retry via message queue
throw $e;
}
}
}
Real-World Impact
Case Study: A B2B merchant integrated with SAP synchronously during checkout. Average order placement time: 8 seconds. During peak hours, SAP API latency increased to 15 seconds, causing customer frustration and 23% cart abandonment. After migrating to message queues, order placement dropped to <2 seconds and abandonment decreased to 12%.
Anti-Pattern 3: Client-Side Price Manipulation
The Wrong Way
// Magento_Checkout/web/js/view/summary/grand-total.js (WRONG)
define([
'Magento_Checkout/js/view/summary/abstract-total',
'Magento_Checkout/js/model/quote'
], function (Component, quote) {
'use strict';
return Component.extend({
getValue: function () {
var total = quote.totals().grand_total;
// SECURITY ISSUE: Applying discount client-side only
if (this.hasSpecialCoupon()) {
total = total * 0.5; // 50% off
}
return this.getFormattedPrice(total);
},
hasSpecialCoupon: function () {
return quote.getCouponCode() === 'SPECIAL50';
}
});
});
Why This Is Wrong
- Security Vulnerability - Client-side prices can be manipulated via browser dev tools
- Price Mismatch - Frontend shows one price, backend charges another
- Revenue Loss - Attackers can set arbitrary prices
- Data Integrity - Quote totals don't match order totals
- PCI Compliance - Fails security audits
The Correct Way
All pricing logic must be server-side:
Total Collector:
namespace Vendor\Module\Model\Quote\Address\Total;
use Magento\Quote\Model\Quote\Address\Total\AbstractTotal;
class SpecialDiscount extends AbstractTotal
{
protected $_code = 'special_discount';
public function __construct(
private readonly \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency
) {}
/**
* Collect special discount total
*/
public function collect(
\Magento\Quote\Model\Quote $quote,
\Magento\Quote\Api\Data\ShippingAssignmentInterface $shippingAssignment,
\Magento\Quote\Model\Quote\Address\Total $total
): self {
parent::collect($quote, $shippingAssignment, $total);
// SERVER-SIDE validation and calculation
if ($this->isValidSpecialCoupon($quote->getCouponCode())) {
$discount = $total->getSubtotal() * 0.5;
$total->setSpecialDiscount(-$discount);
$total->setGrandTotal($total->getGrandTotal() - $discount);
$total->setBaseGrandTotal($total->getBaseGrandTotal() - $discount);
$quote->setSpecialDiscount(-$discount);
}
return $this;
}
/**
* Validate coupon code server-side
*/
private function isValidSpecialCoupon(string $code): bool
{
return $code === 'SPECIAL50' && $this->isCouponNotExpired();
}
/**
* Fetch for totals display
*/
public function fetch(
\Magento\Quote\Model\Quote $quote,
\Magento\Quote\Model\Quote\Address\Total $total
): array {
$discount = $quote->getSpecialDiscount();
if (!$discount) {
return [];
}
return [
'code' => $this->getCode(),
'title' => __('Special Promotion'),
'value' => $discount
];
}
}
Frontend (Read-Only Display):
define([
'Magento_Checkout/js/view/summary/abstract-total',
'Magento_Checkout/js/model/quote'
], function (Component, quote) {
'use strict';
return Component.extend({
/**
* Display server-calculated total (read-only)
*/
getValue: function () {
var totals = quote.getTotals()();
if (totals) {
return this.getFormattedPrice(totals.grand_total);
}
return this.getFormattedPrice(0);
}
});
});
Real-World Impact
Case Study: An attacker discovered client-side discount logic could be manipulated. Using browser dev tools, they set grand_total = 0.01 for high-value orders. Over 48 hours, 47 orders totaling $127,000 were placed for $0.47 total. The vulnerability existed for 6 months before discovery.
Anti-Pattern 4: Storing Sensitive Data in Checkout Session
The Wrong Way
namespace Vendor\Module\Model;
class CustomCheckout
{
public function __construct(
private readonly \Magento\Checkout\Model\Session $checkoutSession
) {}
/**
* Store credit card data in session (WRONG!)
*/
public function saveCreditCard(array $cardData): void
{
// SECURITY VIOLATION: PCI DSS non-compliance
$this->checkoutSession->setData('credit_card_number', $cardData['number']);
$this->checkoutSession->setData('credit_card_cvv', $cardData['cvv']);
$this->checkoutSession->setData('credit_card_expiry', $cardData['expiry']);
}
public function getCreditCardNumber(): string
{
return $this->checkoutSession->getData('credit_card_number');
}
}
Why This Is Wrong
- PCI DSS Violation - Storing full card numbers is prohibited
- Session Storage - Sessions may be stored in database or Redis (unencrypted)
- Session Fixation - Attackers could steal session IDs to access card data
- Compliance Risk - Can result in loss of payment processing privileges
- Legal Liability - Data breach exposes merchant to lawsuits and fines
The Correct Way
Use tokenization and never store sensitive data:
namespace Vendor\Payment\Model;
class TokenManager
{
public function __construct(
private readonly \Vendor\Payment\Gateway\TokenizeClient $tokenizeClient,
private readonly \Magento\Checkout\Model\Session $checkoutSession,
private readonly \Magento\Framework\Encryption\EncryptorInterface $encryptor
) {}
/**
* Tokenize card and store only token
*
* @param array $cardData
* @return string Token
*/
public function tokenizeCard(array $cardData): string
{
// Send card data directly to payment gateway (PCI-compliant)
$token = $this->tokenizeClient->createToken($cardData);
// Store only token in session
$this->checkoutSession->setData('payment_token', $token);
// Store last 4 digits for display (safe)
$this->checkoutSession->setData(
'card_last_four',
substr($cardData['number'], -4)
);
// Never store full card number, CVV, or expiry
return $token;
}
/**
* Get payment token from session
*/
public function getToken(): ?string
{
return $this->checkoutSession->getData('payment_token');
}
/**
* Clear sensitive data after order placement
*/
public function clearPaymentData(): void
{
$this->checkoutSession->unsetData('payment_token');
$this->checkoutSession->unsetData('card_last_four');
}
}
Payment Method Implementation:
namespace Vendor\Payment\Model\Method;
use Magento\Payment\Model\Method\AbstractMethod;
class Gateway extends AbstractMethod
{
public function authorize(
\Magento\Payment\Model\InfoInterface $payment,
$amount
): self {
// Use token instead of card data
$token = $payment->getAdditionalInformation('payment_token');
if (!$token) {
throw new \Magento\Framework\Exception\LocalizedException(
__('Payment token is missing.')
);
}
// Send token to gateway for authorization
$response = $this->gatewayClient->authorize($token, $amount);
// Store only transaction ID
$payment->setTransactionId($response['transaction_id']);
$payment->setIsTransactionClosed(false);
// Never log or store card data
return $this;
}
}
Real-World Impact
Case Study: A payment module stored full credit card numbers in checkout session (database-backed). A SQL injection vulnerability in a third-party module allowed attackers to dump the session table, exposing 12,000 credit card numbers. Merchant faced $2.8M in fines, legal fees, and compensation. Payment processor revoked merchant account.
Anti-Pattern 5: Overusing around Plugins
The Wrong Way
namespace Vendor\Module\Plugin;
class PaymentInformationManagementPlugin
{
/**
* Wrap entire place order method (WRONG)
*/
public function aroundSavePaymentInformationAndPlaceOrder(
\Magento\Checkout\Api\PaymentInformationManagementInterface $subject,
\Closure $proceed,
int $cartId,
\Magento\Quote\Api\Data\PaymentInterface $paymentMethod,
?\Magento\Quote\Api\Data\AddressInterface $billingAddress = null
): int {
// Pre-processing
$this->logger->info('Starting order placement');
$this->validateCustomRules($cartId);
try {
// Call original method
$orderId = $proceed($cartId, $paymentMethod, $billingAddress);
// Post-processing
$this->logger->info('Order placed successfully');
$this->sendNotification($orderId);
return $orderId;
} catch (\Exception $e) {
$this->logger->error('Order placement failed');
throw $e;
}
}
}
Why This Is Wrong
- Plugin Chain Breaking - Other plugins may not execute correctly
- Complexity - Hard to debug when multiple
aroundplugins exist - Performance - Adds overhead even if just logging
- Maintainability - Future developers must understand
\Closuremechanics - Testing - Difficult to test in isolation
The Correct Way
Use before and after plugins:
namespace Vendor\Module\Plugin;
class PaymentInformationManagementPlugin
{
public function __construct(
private readonly \Psr\Log\LoggerInterface $logger,
private readonly \Vendor\Module\Service\Validator $validator,
private readonly \Vendor\Module\Service\NotificationService $notificationService
) {}
/**
* Pre-processing with before plugin
*/
public function beforeSavePaymentInformationAndPlaceOrder(
\Magento\Checkout\Api\PaymentInformationManagementInterface $subject,
int $cartId,
\Magento\Quote\Api\Data\PaymentInterface $paymentMethod,
?\Magento\Quote\Api\Data\AddressInterface $billingAddress = null
): void {
$this->logger->info('Starting order placement', ['cart_id' => $cartId]);
$this->validator->validateCustomRules($cartId);
}
/**
* Post-processing with after plugin
*/
public function afterSavePaymentInformationAndPlaceOrder(
\Magento\Checkout\Api\PaymentInformationManagementInterface $subject,
int $orderId
): int {
$this->logger->info('Order placed successfully', ['order_id' => $orderId]);
$this->notificationService->sendNotification($orderId);
return $orderId;
}
}
Use around only when necessary:
/**
* Use around plugin ONLY when you need to:
* 1. Conditionally skip original method
* 2. Modify arguments before calling original
* 3. Transform return value based on original result
*/
public function aroundSavePaymentInformationAndPlaceOrder(
\Magento\Checkout\Api\PaymentInformationManagementInterface $subject,
\Closure $proceed,
int $cartId,
\Magento\Quote\Api\Data\PaymentInterface $paymentMethod,
?\Magento\Quote\Api\Data\AddressInterface $billingAddress = null
): int {
// Skip original method if order already exists
if ($this->isDuplicateOrder($cartId)) {
return $this->getExistingOrderId($cartId);
}
// Call original method
return $proceed($cartId, $paymentMethod, $billingAddress);
}
Anti-Pattern 6: Ignoring Quote State Validation
The Wrong Way
namespace Vendor\Module\Model;
class CustomOrderPlacer
{
public function __construct(
private readonly \Magento\Quote\Api\CartManagementInterface $cartManagement
) {}
/**
* Place order without validation (WRONG)
*/
public function placeOrder(int $quoteId): int
{
// No validation of quote state
return $this->cartManagement->placeOrder($quoteId);
}
}
Why This Is Wrong
- Race Conditions - Quote may be already converted to order
- Invalid State - Quote may be inactive, merged, or deleted
- Data Integrity - Items may have changed since last validation
- Inventory Issues - Products may be out of stock
- Price Changes - Prices may have been updated
The Correct Way
Validate quote state before operations:
namespace Vendor\Module\Model;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\NoSuchEntityException;
class QuoteValidator
{
public function __construct(
private readonly \Magento\Quote\Api\CartRepositoryInterface $quoteRepository,
private readonly \Magento\CatalogInventory\Api\StockStateInterface $stockState,
private readonly \Psr\Log\LoggerInterface $logger
) {}
/**
* Validate quote before placing order
*
* @param int $quoteId
* @throws LocalizedException
* @throws NoSuchEntityException
*/
public function validate(int $quoteId): void
{
// Load quote
$quote = $this->quoteRepository->getActive($quoteId);
// Validate quote is active
if (!$quote->getIsActive()) {
throw new LocalizedException(
__('The cart is no longer active.')
);
}
// Validate quote has items
if (!$quote->hasItems() || $quote->getItemsCount() === 0) {
throw new LocalizedException(
__('The cart is empty.')
);
}
// Validate quote has no errors
if ($quote->getHasError()) {
throw new LocalizedException(
__('The cart contains errors. Please review your cart.')
);
}
// Validate minimum order amount
if (!$quote->validateMinimumAmount()) {
throw new LocalizedException(
__('Minimum order amount not met.')
);
}
// Validate shipping address (non-virtual)
if (!$quote->isVirtual()) {
$shippingAddress = $quote->getShippingAddress();
if (!$shippingAddress || !$shippingAddress->getShippingMethod()) {
throw new LocalizedException(
__('Please specify a shipping method.')
);
}
}
// Validate payment method
$payment = $quote->getPayment();
if (!$payment || !$payment->getMethod()) {
throw new LocalizedException(
__('Please specify a payment method.')
);
}
// Validate inventory availability
$this->validateInventory($quote);
$this->logger->info('Quote validation passed', [
'quote_id' => $quoteId
]);
}
/**
* Validate inventory availability
*/
private function validateInventory(\Magento\Quote\Model\Quote $quote): void
{
foreach ($quote->getAllItems() as $item) {
if ($item->getParentItem()) {
continue;
}
$stockStatus = $this->stockState->getStockQty(
$item->getProduct()->getId(),
$item->getProduct()->getStore()->getWebsiteId()
);
if ($stockStatus < $item->getQty()) {
throw new LocalizedException(
__('Product "%1" is no longer available in requested quantity.', $item->getName())
);
}
}
}
}
Anti-Pattern 7: Hardcoding Configuration Values
The Wrong Way
namespace Vendor\Module\Model;
class ShippingCalculator
{
/**
* Calculate shipping (HARDCODED VALUES)
*/
public function calculate(float $weight, string $country): float
{
// WRONG: Hardcoded values
$baseRate = 5.00;
$perKgRate = 2.50;
$internationalSurcharge = 10.00;
$rate = $baseRate + ($weight * $perKgRate);
if ($country !== 'US') {
$rate += $internationalSurcharge;
}
return $rate;
}
}
Why This Is Wrong
- No Configurability - Merchant cannot change rates without code changes
- Multi-Store Issues - Cannot have different rates per store
- Deployment Required - Rate changes require code deployment
- No Audit Trail - Cannot track when/who changed rates
- Testing Difficulty - Hard to test with different rate scenarios
The Correct Way
Use system configuration:
System Config (system.xml):
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
<system>
<section id="shipping">
<group id="vendor_shipping" translate="label" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Custom Shipping Rates</label>
<field id="base_rate" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Base Rate</label>
<validate>required-entry validate-number validate-zero-or-greater</validate>
</field>
<field id="per_kg_rate" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Per Kilogram Rate</label>
<validate>required-entry validate-number validate-zero-or-greater</validate>
</field>
<field id="international_surcharge" translate="label" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1">
<label>International Surcharge</label>
<validate>required-entry validate-number validate-zero-or-greater</validate>
</field>
</group>
</section>
</system>
</config>
Implementation:
namespace Vendor\Module\Model;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Store\Model\ScopeInterface;
class ShippingCalculator
{
private const XML_PATH_BASE_RATE = 'shipping/vendor_shipping/base_rate';
private const XML_PATH_PER_KG_RATE = 'shipping/vendor_shipping/per_kg_rate';
private const XML_PATH_INTL_SURCHARGE = 'shipping/vendor_shipping/international_surcharge';
public function __construct(
private readonly ScopeConfigInterface $scopeConfig
) {}
/**
* Calculate shipping (CONFIGURABLE)
*/
public function calculate(float $weight, string $country, ?int $storeId = null): float
{
$baseRate = $this->getConfigValue(self::XML_PATH_BASE_RATE, $storeId);
$perKgRate = $this->getConfigValue(self::XML_PATH_PER_KG_RATE, $storeId);
$internationalSurcharge = $this->getConfigValue(self::XML_PATH_INTL_SURCHARGE, $storeId);
$rate = $baseRate + ($weight * $perKgRate);
if ($country !== 'US') {
$rate += $internationalSurcharge;
}
return $rate;
}
/**
* Get configuration value
*/
private function getConfigValue(string $path, ?int $storeId = null): float
{
return (float)$this->scopeConfig->getValue(
$path,
ScopeInterface::SCOPE_STORE,
$storeId
);
}
}
Summary of Critical Anti-Patterns
| Anti-Pattern | Risk Level | Impact | Correct Approach |
|---|---|---|---|
| Modifying core files | CRITICAL | Upgrade path broken | Use plugins/observers |
| Blocking observers | HIGH | Performance degradation | Use message queues |
| Client-side pricing | CRITICAL | Security vulnerability | Server-side totals only |
| Storing card data | CRITICAL | PCI non-compliance | Use tokenization |
Overusing around plugins |
MEDIUM | Maintainability issues | Use before/after |
| Skipping validation | HIGH | Data integrity issues | Validate quote state |
| Hardcoding config | MEDIUM | No flexibility | Use system config |
Assumptions
- Target Platform: Adobe Commerce & Magento Open Source 2.4.7+
- PHP Version: 8.1, 8.2, 8.3
- Deployment: Production environment with proper code review
- Security: PCI DSS compliance required for payment processing
Why These Are Anti-Patterns
- Upgrade Path: Following best practices ensures smooth upgrades
- Security: Proper patterns prevent vulnerabilities and compliance violations
- Performance: Asynchronous processing improves checkout speed
- Maintainability: Standard patterns are easier for teams to understand
- Testability: Properly structured code is easier to test
Security Impact
- PCI Compliance: Never store sensitive payment data in application
- Price Integrity: All pricing calculations must be server-side
- Input Validation: Always validate and sanitize user input
- Session Security: Use secure session configuration and HTTPS
Performance Impact
- Async Processing: Use message queues for non-critical operations
- Database Queries: Minimize queries in checkout flow
- External APIs: Never block checkout on external service calls
- Caching: Cache configuration values and static data
Backward Compatibility
- Plugin System: Plugins preserve upgrade path
- Service Contracts: Use service contracts for stable APIs
- Configuration: System config allows runtime changes without deployment
- Event System: Events provide stable extension points
Tests to Add
- Unit Tests: Test business logic with mocked dependencies
- Integration Tests: Test actual checkout flow
- Security Tests: Verify no sensitive data leakage
- Performance Tests: Measure checkout completion time
- Regression Tests: Ensure anti-patterns don't reappear
Documentation to Update
- Code Review Checklist: Include anti-pattern checks
- Developer Guide: Document correct patterns
- Security Guide: Highlight compliance requirements
- Architecture Guide: Explain why patterns are anti-patterns