The function I was happiest to delete
The best code I ever deleted was a utility function called format_response. It took a response object, a format string, a fallback value, an optional transformer function and a boolean for stripping whitespace. It was used in exactly one place.
I wrote it because I thought I would need it again. I didn't. It sat in utils.py for eight months, collecting type: ignore comments and confusing everyone who opened the file.
This post covers the abstractions I built too early, the patterns I reached for out of habit, and the DRY violations that should have stayed wet.
The generic service layer that served nobody
Early in Polaris, I built a generic service layer. Every model would get a service class with standard CRUD operations, validation hooks and permission checks. Write it once, inherit everywhere.
class BaseService:
model = None
serializer = None
def create(self, data, **kwargs):
self._validate(data)
self._check_permissions(kwargs.get('user'))
instance = self.model.objects.create(**data)
self._post_create(instance)
return instance
def _validate(self, data):
pass # Override in subclass
def _check_permissions(self, user):
pass # Override in subclass
def _post_create(self, instance):
pass # Override in subclassEvery subclass overrode every method. ProductService._validate had nothing in common with CustomerService._validate. The shared base class was an empty method contract. It added indirection and removed no duplication.
It got worse when I needed behavior outside create, read, update and delete. Polaris has FIFO inventory consumption and double-entry ledger operations, and the service layer fought both. The abstraction assumed every operation is CRUD. Financial operations are not.
What replaced it
Individual service classes with no shared base. LedgerService has credit_customer and reverse. InventoryService has consume_inventory and receive_batch. They share nothing because they do nothing in common.
By DRY standards the code is less clean. It is much easier to understand and change.
LangChain memory vs. a Redis list
LangChain ships memory abstractions: ConversationBufferMemory, ConversationSummaryMemory, ConversationEntityMemory. They look good in tutorials. In production they caused me real problems:
- Memory is in-process by default. Restart the server and all conversation history is gone.
- No TTL. Chat histories grow without limit. One power user with a 200-message conversation now holds a meaningful amount of memory.
- The memory object isn't thread-safe. Two concurrent requests to the same conversation can corrupt it.
- Serialization is fragile. Switching model providers broke deserialization because the message formats differ.
The fix was embarrassingly simple:
import json
import redis
r = redis.Redis()
def get_history(session_id: str, max_messages: int = 50) -> list[dict]:
raw = r.lrange(f"chat:{session_id}", -max_messages, -1)
return [json.loads(m) for m in raw]
def add_message(session_id: str, role: str, content: str):
r.rpush(f"chat:{session_id}", json.dumps({
"role": role, "content": content
}))
r.expire(f"chat:{session_id}", 86400) # 24h TTLFifteen lines. It survives restarts, has a TTL, is safe under concurrent requests and serializes predictably.
I spent a week debugging LangChain memory before I wrote this. The abstraction cost me that week, because its failure modes were hidden behind three layers of class inheritance.
The principle
If you can explain the solution in one sentence, you probably don't need an abstraction layer. "Store messages in a Redis list with a TTL" is one sentence. ConversationSummaryBufferMemory(llm=llm, max_token_limit=2000, return_messages=True) is a configuration surface with hidden semantics.
Django signals for everything
Signal cascades have a performance cost, but that was the second-worst thing about how I used them. The worst was debugging.
At one point Polaris had 23 signal handlers across 8 files. Creating a sale triggered this chain:
post_saveonSale→ update inventorypost_saveonProduct(from step 1) → recalculate stock alertspost_saveonStockAlert(from step 2) → notify supplierpost_saveonSaleagain → update customer balance- Custom signal
balance_changed→ invalidate cached reports
Tracing a bug through that is miserable. There is no call stack and no explicit invocation. grep finds the handler but not what triggers it. Signals are implicit coupling that looks like decoupling.
What replaced it
Explicit function calls.
class SaleService:
def complete_sale(self, sale):
with transaction.atomic():
self._deduct_inventory(sale)
self._update_customer_balance(sale)
self._invalidate_reports(sale.customer_id)By separation-of-concerns standards this is worse: SaleService now knows about inventory and reporting. But anyone who reads complete_sale can follow the whole execution path without leaving the function. That is worth more than architectural purity.
When signals are actually good
Signals fit cross-app boundaries where loose coupling matters. A billing app doesn't need to know about an analytics app, so a signal fired on "payment completed" is fine when the handler lives in a different bounded context.
Inside one app's core business logic, signals make the code harder to follow for whoever reads it next.
The reusable component library
On a frontend project, I built a component library before I had any components to put in it. A BaseCard with 12 props. A BaseButton with configurable size, variant, icon position, loading state and a disabled tooltip.
BaseCard ended up used in 3 places across the whole project, each with a completely different layout that made the base props irrelevant. Two of the three passed so many overrides that the component was a <div> with extra steps.
The button was worse. I added a tooltipPosition prop because one button needed a left-aligned tooltip. After that, every button in the system carried tooltip positioning logic it never used. The API grew to cover every edge case, so every consumer had to understand all of it just to render a button.
What I do now
I start with plain HTML elements. When I have three genuinely similar components that share non-trivial logic, I extract the shared part at that point.
<!-- Three similar buttons? Copy-paste is fine until it isn't. -->
<button class="btn-primary" @click="save">Save</button>
<button class="btn-primary" @click="submit">Submit</button>
<button class="btn-primary" :disabled="loading" @click="confirm">
{{ loading ? 'Confirming...' : 'Confirm' }}
</button>The third button has a loading state. One button with a loading prop covers it.
The config-driven architecture
At one point I tried to make Polaris configurable by moving business rules into configuration objects:
INVENTORY_CONFIG = {
"costing_method": "fifo",
"allow_negative_stock": False,
"reorder_threshold_multiplier": 1.5,
"batch_tracking_enabled": True,
"auto_reorder_enabled": False,
}The idea was that different clients could change behavior without code changes.
In practice, every configuration option eventually needed code changes anyway. "Allow negative stock" sounds like a boolean, but negative-stock logic is fundamentally different from positive-only logic. Flipping the flag means taking a different code path, with its own validation, its own reporting and different financial consequences.
I ended up with code full of if settings.INVENTORY_CONFIG["allow_negative_stock"]: branches. Each one needed its own tests and each one was a place for bugs to hide. The config didn't remove complexity. It spread it across every function that read it.
What replaced it
Hard-coded business rules that match the actual client's requirements. When a new client needs different behavior, I decide whether it is a real variation or a different product. Usually it is a different product.
Configuration is for deployment parameters: database URLs, API keys, feature flags for A/B tests. Business rules are code. They deserve tests, type checking and code review, and none of those work well on JSON objects.
The pattern I keep coming back to
Every abstraction I regret has the same origin: I built it for a need I imagined. The generic service layer was for "when we have 50 models." The config-driven architecture was for "when we have multiple clients." The component library was for "when we have a design system."
None of those futures arrived the way I pictured them. When they arrived at all, the real requirements were different enough that the abstraction didn't fit.
The abstractions I don't regret were all extracted, never invented. I wrote the code three times, noticed the pattern and pulled it out. TenantAwareQuerySet in Polaris started as copy-pasted .filter(organization=org) calls. Once 15 models were doing the same filter, the abstraction was obvious and correct.
My rule now: if you can't point to three existing call sites that would use the abstraction, you're speculating. Speculative code is expensive because it stays until someone is brave enough to delete it.
Deleting an abstraction is also harder than deleting ordinary code. Ordinary code has no dependents. An abstraction has consumers, and each one was shaped by its API. Removing it means refactoring every one of them, so the abstraction calcifies.
So when you are about to create BaseService, GenericHandler or AbstractProcessor, write the specific thing first. Write it again when you need it again. By the third time you will know what the abstraction actually is.