A stray commit buried multiple levels deep cost me months

Aug 02, 2026 06:19 AM - 2 hours ago 4

I’ve been moving connected a spec for months. Finally…every stakeholder approves. I’ve cataloged each portion of codification that needs to beryllium migrated successful a Notion DB. I’m moving connected 1 of the first PRs and past I spot it…

“…ffffuuuuuummmmccckkkk mmmmmmeee…”

The fucking “transactions” are being committed while wearing a overgarment of atomicity.

“fuck, fuck, fuck, really the fuck did I miss this.”

Looking done my notes…I see…“investigate random-ass commits” and I forgot to relationship for them…

  • ORMs tin rustle your limb off, this ain’t that rant.
  • Parametrized SQL statements tin beryllium your saviour, this ain’t that rant.
  • Query builders tin beryllium a bully mediate ground, this isn’t that rant.

This rant is astir fucking up codification statement and abstractions truthful severely that codification wrapped wrong transactions ain’t atomic.

Sample Exhibits

These are meant to beryllium illustrative, truthful codification has been cleaned up and doesn’t incorporate each the mini specifications of immoderate framework/library. Sessions being created, etc.

The Hidden Enemy

class DBAccess: @staticmethod def create_records(records: List[DomainModel]): # All records are meant to beryllium a azygous transaction with transaction(): for r in records: DBAccess.create_main_records(r) # calls helpers that yet telephone commit() # Another transaction is auto-started DBAccess.create_details_records(r) DBAccess.create_records(recs)

The codification is fucking astir pinch manual commits 2+ levels distant from the transaction decorator/context manager. So, erstwhile create_main_records is called from the manager, nary 1 has ANY deity damn thought that the method commits.

The Silent Frenemy

class DBAccess: @staticmethod def fetch_records(ids: List[int]) -> List[DBModel]: db_models = session.query(DBModel).filter(DBModel.id.in_(ids)).all() return cast(List[DBModel], db_models) with transaction(): db_models = DBAccess.fetch_records(ids) db_models[0].yo_mama_fat = True # This is simply a silent DB write # discourse head exit saves your ass

The codification is passing DB models around, treating them for illustration normal domain models, mounting properties that look to do thing different than mounting the worth but underneath it all, location are DB writes being triggered.

Daddy’s Milk Run

class DBAccess: @staticmethod def fetch_records(ids: List[int]) -> List[DBModel]: db_models = session.query(DBModel).filter(DBModel.id.in_(ids)).all() return cast(List[DBModel], db_models) # transaction has been commented out # pinch transaction(): db_models = DBAccess.fetch_records(ids) db_models[0].yo_mama_fat = True # petition ends, information poofs into the ether # for illustration the programmer's dada leaving to get beverage and ne'er returning

The codification doesn’t auto-commit and has nary transactions, it’s causing information loss.

Want to perceive much of my unhinged rants? Drop your email.

or usage the RSS feed if you dislike email.

Who is responsible?

  • DO NOT blasted the framework
  • DO NOT blasted the business requirement.

The programmer is the only 1 responsible. In a batch of cases, you tin beryllium absolved of the blasted for the authorities of the code, but NOT successful this case. ANYTHING is amended than random ass commits.

In the end, your ass, whether you wrote the codification aliases not (my case) is connected the statement for fixing this shit. Like I person been for months.

WTF are we learning?

  1. DO NOT sprinkle successful DB sessions aliases transactions for illustration salt-bae. If you do so, expect to clang and pain for illustration him. The DB abstraction furniture owns the transactions and commits.
  2. DO NOT walk DB models successful and retired of the DB layer.
  3. Read 1+2 again, past publication them again. Absorb them.
  4. DO NOT fuck pinch transactions, commits, queries aliases immoderate extracurricular of the DB layer. Don’t moreover look astatine it, don’t moreover deliberation astir it.
  5. DO NOT perpetrate manually. Especially if you usage discourse managers aliases decorators. You are not that guy/gal pal.
  6. DO NOT divided codification crossed helpers. Atomic multi-writes = 1 function. Write the 3 statement god-damn insert again. The plagiarism keeps atomicity visible and you won’t create hidden enemies. Otherwise, watch your god-damn back.

How do you enforce the lessons?

AST Analysis:

Either done civilization tests that usage AST study aliases done flake8/linters. Whatever you choose, it should:

  • Ban commits from being called manually, COMPLETELY
  • Ban db convention from being accessed extracurricular the DB entree layer
  • Ban transactions from being accessed extracurricular the DB entree layer
  • Ban DB exemplary imports from extracurricular the DB entree layer

Bans utilizing AST

class TestDBBoundaries: def test_no_manual_commits(self): violations = [] for path, character in parsed_source_files(): # ast.parse complete your root tree for node in ast.walk(tree): if not isinstance(node, ast.Call): continue func = node.func if ( (isinstance(func, ast.Attribute) and func.attr == "commit") # session.commit() or (isinstance(func, ast.Name) and func.id == "commit") # perpetrate = session.commit; commit() ): violations.append(f"{path}:{node.lineno} manual commit()") assert not violations, "\n".join(violations)

Bans utilizing flake8

Same walk, wearing flake8’s trench coat:

import ast class BanManualCommits: def __init__(self, tree): self.tree = tree def run(self): for node in ast.walk(self.tree): if not isinstance(node, ast.Call): continue func = node.func if ( (isinstance(func, ast.Attribute) and func.attr == "commit") or (isinstance(func, ast.Name) and func.id == "commit") ): yield node.lineno, node.col_offset, "DB001 manual commit()", type(self)

What the registration looks like:

[project.entry-points."flake8.extension"] DB0 = "flake8_db_boundaries:BanManualCommits"

AST aliases flake8?

Use AST tests when:

  • the norm needs to look astatine the full codebase
  • you can’t touch the shared linter config origin that’ll (rightfully) callout different teams’ shitty codification and egos are excessively fragile

Otherwise instrumentality pinch flake8/linters. They are the modular devices for a reason.

Combined pinch LLM Support

LLMs should beryllium utilized to:

  • Ban immoderate DB models from being returned by the DB layer

AST can’t drawback this 1 and neither tin mypy, cause:

  • return annotations lie
  • cast() launders types (The Silent Frenemy)
  • a codebase pinch random-ass commits doesn’t animate assurance successful the typing

So this cheque is an LLM walk successful the CI/CD pipeline. A deterministic book dumps the DB entree layer’s nationalist functions/methods, past the LLM answers a azygous question: “does thing return a DB exemplary alternatively of a domain model?” No agentic drama, create a punctual and propulsion the output from the book successful there. Results:

  • yes -> human review
  • maybe -> quality review
  • no -> walk Go, cod your paycheque

✌️

If you find maine insufferable, fine. The one return away:

The DB abstraction furniture owns the commits and transactions. Everything other is simply a workaround for getting that wrong.

So, you don’t person to walk MONTHS refactoring the full god-damn codebase to hole fucked up transactions aft discovering a random-ass commit.

ALSO READ THIS GOD-DAMN BOOK: Domain-Driven Design: Tackling Complexity successful the Heart of Software.

Want to perceive much of my unhinged rants? Drop your email.

or usage the RSS feed if you dislike email.

More