The "Shallow" Trap: Why Your Outbox Pattern is Costing You Cognitive Load
As architects, we’re often told that complexity is the price we pay for scale. We implement patterns like the Transactional Outbox to solve the "Dual Write" problem, ensuring that our database and our message broker (like Kafka or RabbitMQ) stay in perfect sync.
But here is the hard truth: Most Outbox implementations are actually "Shallow" modules. They solve a distributed systems problem, but they create a cognitive load problem.
If you want to build systems that last, you need to stop writing tactical code and start designing Deep Modules.
1. The "Shallow" Outbox (The Tactical Approach)
A "Shallow" module is one that has a complex interface but does very little. It forces the developer to manually orchestrate every step.
In a typical backend service, a shallow Outbox implementation looks like this:
// The developer is forced to manage the infrastructure "plumbing"
func CreateOrder(db *sql.DB, order Order) error {
tx, err := db.Begin()
defer tx.Rollback()
// 1. Save business data
if err := orderRepo.Save(tx, order); err != nil {
return err
}
// 2. Manually construct the event
event := OrderCreatedEvent{ID: order.ID, Total: order.Total}
// 3. Manually save to the Outbox table
if err := outboxRepo.Save(tx, event); err != nil {
return err
}
return tx.Commit()
}
The Problem: The business logic is now "leaking" infrastructure concerns. The developer has to remember to start the transaction, save the event, and commit. If they forget a single step, the system fails. This is Shallow Architecture—it's fragile, and it drains your mental RAM.
2. The "Deep" Outbox (The Strategic Approach)
In his book A Philosophy of Software Design, John Ousterhout argues that the best modules are Deep: they have a simple interface that hides a great deal of internal complexity.
A Deep Outbox module doesn't ask the developer to manage transactions or tables. It provides a single, clean abstraction:
// The business logic is now pure and effortless
func CreateOrder(order Order) error {
return outbox.SaveWithEvent(order, "OrderCreated")
}
Why "Deep" is Better
When you call outbox.SaveWithEvent(), the module handles the heavy lifting "under the hood":
- Internal Transaction Management: It wraps the operation in a transaction automatically.
- Zero-Knowledge Persistence: It knows how to map your object to both the business table and the outbox table.
- Automated Relay: It manages the background process (or CDC connector) that pushes the event to your message bus.
By hiding this complexity, you’ve reduced the "surface area" of your code. You’ve moved from being a Tactical Coder to a Strategic Architect.
The Bottom Line for Architects
Complexity is a zero-sum game. If you don't hide it inside a Deep Module, it will leak into your application logic, making your system harder to maintain and your late-night debugging sessions significantly longer.
Next time you’re implementing a pattern—whether it’s CQRS, Event Sourcing, or a simple Outbox—ask yourself:
"Am I creating a shallow interface that forces the user to think, or a deep one that lets them build?"
Choose depth. Your future self (and your team) will thank you.