#Backend

The Database Dilemma: Keeping Domain Logic Out of Your Data Layer

Tech Essays Reporter
6 min read

A pragmatic approach to database design that emphasizes leveraging database capabilities while maintaining clean separation of concerns between application logic and data storage.

The relationship between application code and databases has long been a source of tension in software architecture. While databases serve as the foundation for persistent data storage, the question of where to place business logic—in the application layer or the database itself—continues to challenge developers. Alex Kondov's recent exploration of this topic offers a nuanced perspective that prioritizes maintainability while still advocating for full utilization of database capabilities.

The fundamental challenge, as Kondov articulates, stems from the immutable nature of data. Unlike application code, which can be completely refactored or rewritten with each deployment, data persists and accumulates over time. Users depend on their data, and corrupted or lost information can have severe consequences beyond simple bug fixes. This permanence creates a natural tension: how do we evolve our systems to meet new business requirements while preserving the integrity of existing data?

Kondov's central thesis is both simple and profound: utilize databases as much as possible without putting domain logic in them. This approach recognizes that while databases excel at data storage and retrieval, they often lack the maintainability, testability, and clarity that application code provides.

The Problem with Database Logic

The appeal of implementing business logic directly in databases is understandable. Triggers, stored procedures, and other database-level mechanisms can respond automatically to data changes, potentially reducing the number of application calls and centralizing related functionality. However, this convenience comes at a significant cost.

When domain logic is split between the application and the database, several problems emerge. First, maintainability suffers dramatically. Engineers working on the application layer may lack the specialized knowledge required to understand and modify database-level logic. This creates a knowledge silo that can slow development and increase the risk of errors.

Second, testing becomes more complex. Database logic often cannot be easily mocked or executed within the same test suite as application code, leading to fragmented testing strategies and reduced confidence in system behavior.

Third, the separation of concerns becomes blurred. Applications are designed to handle user input, produce output, and manage side effects like database calls. When the database starts making decisions about how data should be stored or modified, this clear boundary becomes muddled.

Leveraging Database Capabilities Without Logic

The key insight is that we can take full advantage of database capabilities without embedding business rules within them. Kondov provides several practical examples of this approach.

Consider a social media application where users can like and unlike posts. The business requirement specifies that unlike actions shouldn't delete the record but instead mark it as unliked, preserving data for potential future analysis. One approach would be to implement this logic entirely in the application layer, checking the current status and deciding whether to create, update, or do nothing.

However, a more elegant solution leverages database constraints and conflict resolution. By creating a unique index on the combination of user ID and post ID, we can attempt to insert a new record and let the database handle conflicts. The ON CONFLICT clause can then update the existing record's status, effectively implementing the "like or update" logic in a single database operation.

This approach maintains the separation of concerns while still utilizing the database's strengths. The application simply attempts to mark a post as liked, and the database handles the complexity of whether to create a new record or update an existing one. The logic remains in the application layer, but the database does the heavy lifting of data manipulation.

Query Optimization and Data Access Patterns

Beyond conflict resolution, Kondov emphasizes the importance of optimizing data access patterns. Instead of making multiple queries and filtering results in application code, developers should explore nested queries and database-specific features that can reduce round trips to the database.

For NoSQL databases like DynamoDB, this might mean leveraging secondary indexes and carefully designed table structures rather than relying on post-query filtering. While DynamoDB's FilterExpression parameter offers flexibility, it operates after data retrieval, essentially shifting the filtering work from application code to the database without reducing the amount of data transferred.

Instead, thoughtful table design with appropriate primary and secondary indexes can enable more efficient data access patterns. By aligning the database structure with expected query patterns, developers can minimize the need for complex filtering logic while maintaining good performance.

The Gray Area: Where to Draw the Line

The distinction between database utility and domain logic isn't always clear-cut. Kondov acknowledges this ambiguity, using the ON CONFLICT example to illustrate the nuanced nature of this boundary. The key question becomes: is the database making decisions about when and how something should be stored, or is it simply providing a mechanism to describe what happens under certain conditions?

When the database is used to implement conditional logic based on business rules—such as deciding whether to create or update a record based on its existence—the line has likely been crossed. However, when the database provides a mechanism to handle conflicts or optimize data access without embedding business decisions, it remains within acceptable bounds.

Event-Driven Architecture Considerations

Event-driven systems present additional challenges for this architectural approach. The need to react to data changes often leads developers toward database triggers, but Kondov recommends implementing such functionality entirely in the application layer when possible.

Eventual consistency models, particularly in distributed databases like DynamoDB, complicate this further. The race condition between write acknowledgment and data propagation across partitions means that application logic cannot reliably depend on immediate data availability after a write operation.

In these cases, Kondov suggests using database triggers only when necessary for consistency guarantees, but implementing the actual business logic in separate services or functions. This maintains the separation of concerns while acknowledging the practical requirements of distributed systems.

The Pragmatic Bottom Line

Kondov's conclusion is refreshingly pragmatic: while there may be valid cases where database-level logic is the only reasonable solution, these situations are rare. In the vast majority of cases, developers are better served by keeping domain logic in the application layer while fully utilizing the database's data manipulation and access capabilities.

This approach offers several benefits: improved maintainability through clear separation of concerns, better testability with unified testing strategies, reduced knowledge silos, and more flexible system evolution. As business requirements change, application code can be modified without the constraints and complexities of database-level logic.

The database dilemma ultimately comes down to recognizing the distinct strengths of each layer in your architecture. Databases excel at data persistence, integrity constraints, and optimized data access. Application code excels at business logic, user interaction, and system orchestration. By respecting these boundaries while fully leveraging each layer's capabilities, developers can build systems that are both powerful and maintainable.

This balanced approach doesn't reject database features or treat the database as a simple dumb storage layer. Instead, it advocates for a thoughtful integration where the database does what it does best—managing data—while the application handles the complex business rules that drive your domain. The result is a system architecture that can evolve gracefully over time while maintaining the integrity and reliability that users expect.

Comments

Loading comments...