Business Logic and Maintainable ArchitectureIntroductionBackend software does more than receive requests and read or write database records. It applies the rules that determine what the system is allowed to do. These rules are the business logic of the application. Examples include calculating an order total, deciding whether a customer may withdraw funds, checking whether a booking is available, and determining which users may approve a transaction. When business logic is scattered across controllers, database queries, user-interface code, and utility functions, a system becomes difficult to understand and change. A small business-policy change may require modifications in several unrelated files. Bugs become more likely, automated testing becomes harder, and developers become afraid to improve the code. Maintainable architecture addresses these problems by giving each part of the system a clear responsibility and by controlling dependencies between parts. This chapter introduces layered architecture, MVC, service-repository design, modular design, clean architecture, SOLID principles, reusable components, transactions, idempotency, and architectural trade-offs. The emphasis is practical: a certificate-level backend developer should be able to recognize poor structure, organize a small application sensibly, and make informed design decisions. Learning ObjectivesBy the end of this chapter, you should be able to: - distinguish presentation, application, domain, and infrastructure responsibilities;
- explain how MVC and service-repository architecture organize backend applications;
- identify the characteristics of maintainable and modular code;
- apply the SOLID principles appropriately rather than mechanically;
- design reusable components with clear interfaces;
- explain database transactions, atomicity, consistency, isolation, and durability;
- design idempotent operations for retries and duplicate requests;
- select an architecture proportionate to the size and risk of a system;
- evaluate the ethical, security, and operational consequences of architectural choices.
1. Business Logic as the Centre of an ApplicationBusiness logic consists of rules, decisions, calculations, and workflows that express the purpose of an organization or product. It is different from technical logic. For example, “convert a request body from JSON” is technical logic, whereas “a customer may cancel an order only before it is dispatched” is business logic. A useful distinction is: | Type of logic | Example | Typical location |
|---|
| Presentation logic | Validate that a field is present in an HTTP request | Controller or request layer | | Application logic | Coordinate payment, order creation, and notification | Application service | | Domain logic | Determine whether cancellation is permitted | Domain model or domain service | | Infrastructure logic | Execute SQL or send an email | Repository or adapter |
The distinction is conceptual rather than absolute. A small application may combine some responsibilities, but the developer should still know which kind of logic is being written. Domain rules should not depend unnecessarily on HTTP, a particular database vendor, or a user-interface framework. A well-organized request commonly follows this path: Request → Controller → Application Service → Domain Rules → Repository/External Adapter → Response The controller translates the external request into an internal command. The application service coordinates the use case. Domain objects or domain services apply rules. Repositories and adapters communicate with databases and external systems. The response is then translated back into the protocol expected by the client. This separation has practical benefits: - Change isolation: changing a database library need not change business rules.
- Testability: domain rules can be tested without starting a web server.
- Consistency: the same rule can be used by an HTTP endpoint, a scheduled job, or a command-line process.
- Team cooperation: developers can work on different layers with clearer boundaries.
- Security: authorization and validation responsibilities become visible rather than being hidden in scattered code.
2. Layered Architecture and MVCLayered architectureLayered architecture arranges software into levels, with each level providing services to the level above it. A common backend arrangement is: - Presentation layer: HTTP routes, controllers, serializers, and response formatting.
- Application layer: use-case coordination, workflow sequencing, and transaction boundaries.
- Domain layer: entities, value objects, policies, and business invariants.
- Infrastructure layer: databases, message brokers, file systems, payment gateways, and email providers.
A conventional dependency direction is from the outer layers toward the inner layers. The domain layer should not need to know whether a request arrived through REST, GraphQL, or a background worker. Layers are not necessarily separate servers or folders. They are responsibilities and dependency boundaries. A project can have four folders while still being badly layered if controllers contain SQL and business decisions. Model–View–ControllerMVC divides an application into: - Model: data and domain behaviour;
- View: representation presented to the user or client;
- Controller: receives input and coordinates processing.
In a server-rendered web application, the view may be an HTML template. In an API, the “view” is often a JSON representation. MVC is useful for organizing request handling, but MVC alone does not guarantee good business design. A controller can become a “fat controller” containing validation, calculations, SQL, and external calls. The controller should normally remain thin and delegate meaningful work to application services. Service-repository architectureA service represents an application operation or use case, such as PlaceOrder, RegisterUser, or ApproveLoan. A repository provides persistence-oriented operations, such as finding an order or saving a customer. The service should express the workflow; the repository should hide storage details. For example, an order service may: - load the customer and product information;
- verify availability and customer permissions;
- calculate the total using domain rules;
- create an order;
- save the order within a transaction;
- publish a confirmation event after successful commitment.
The repository should not decide whether a customer is allowed to buy an item. That is a business decision. Similarly, the controller should not know the SQL used to find the customer. Repositories are abstractions, not automatically beneficial interfaces. Creating an interface for every table without a meaningful purpose can add complexity. A repository is most valuable when it protects the domain from persistence details, supports testing, or represents a meaningful aggregate or query boundary. 3. Modular Design and CohesionA module is a unit of code organized around a coherent responsibility. A module may be a package, library, service, or feature directory. Good modules have: - high cohesion: related responsibilities belong together;
- low coupling: the module depends on few details of other modules;
- explicit interfaces: other code uses documented operations rather than internal variables;
- encapsulation: implementation details can change without breaking consumers.
Feature-based organization often works well for growing applications. Instead of placing all controllers in one directory and all services in another, a project may group code by business capability: orders/
order_controller
order_service
order_entity
order_repository
payments/
payment_controller
payment_service
payment_gateway This structure makes it easier to find the code for a use case. It does not remove the need for architectural layers; rather, each feature can contain its own presentation, application, domain, and infrastructure elements. Avoid excessive sharing. A common “utils” directory often becomes a dumping ground for unrelated functions. A component should be shared when its meaning, ownership, and rules are genuinely common. Copying a small, stable function may sometimes be safer than creating a highly general abstraction that serves no clear purpose. Domain entities and value objectsAn entity has an identity that remains meaningful even if its attributes change. An order with identifier O-104 is an entity. A value object is defined by its attributes rather than identity. A money amount with currency, an email address, or a date range may be a value object. Value objects can improve correctness by validating their own constraints. A Money object may reject a negative amount where negative values are not meaningful. A DateRange may ensure that its end is not before its start. Such objects reduce repeated validation and make method signatures clearer. An invariant is a condition that must remain true. Examples include “an order cannot be paid twice” or “an account balance cannot become negative.” Invariants should be protected at the boundary where the relevant state changes, not merely checked in a user interface. 4. Clean Architecture and Dependency InversionClean architecture organizes a system into concentric responsibilities. Names differ among implementations, but the central idea is stable: - entities and domain rules are at the centre;
- use cases coordinate application-specific actions;
- interface adapters translate between the application and external formats;
- frameworks and infrastructure are replaceable outer details.
The Dependency Rule states that source-code dependencies should point toward policies and stable business rules, not toward volatile technical details. A use case may depend on a PaymentGateway interface, while a concrete Stripe or bank adapter depends on that interface. The use case does not depend directly on the payment vendor’s SDK. This is an application of Dependency Inversion. The direction of runtime control and the direction of source-code dependency need not be identical. At runtime, a use case calls a payment gateway. At design time, both the use case and the adapter conform to an interface owned by the application boundary. Clean architecture is valuable when a system has significant business rules, multiple delivery mechanisms, long expected life, or high testing requirements. It can be excessive for a small CRUD application whose main purpose is to store and retrieve simple records. Architecture should serve the problem rather than become an ideological goal. 5. SOLID Principles in Backend DevelopmentSOLID is a set of design principles. They are guidelines for managing change, not rigid laws. Single Responsibility PrincipleA module should have one coherent reason to change. A controller that parses HTTP, calculates prices, writes SQL, sends email, and formats error responses has several reasons to change. Separating these responsibilities makes changes safer. The principle does not mean every function must contain one line or every class must do only one tiny action. “Responsibility” refers to a meaningful area of change. Open–Closed PrincipleSoftware entities should be open for extension but closed for modification where practical. For example, a discount calculation may use a DiscountPolicy interface. Adding a seasonal policy can then involve a new implementation rather than editing a large conditional statement. This principle should not produce abstractions for every possible future. A stable variation point is a better reason for an interface than speculation. Liskov Substitution PrincipleA subtype should be usable wherever its abstraction is expected without breaking correctness. If a repository contract promises that save persists an object, an implementation that silently discards it violates the contract. If a method accepts any payment method but one subtype unexpectedly throws “not supported,” the abstraction may be poorly designed. Interface Segregation PrincipleClients should not be forced to depend on methods they do not use. A large UserOperations interface containing authentication, reporting, billing, and profile methods can be split into smaller role-specific interfaces. Smaller interfaces are easier to implement, test, and understand. Dependency Inversion PrincipleHigh-level policy should not depend directly on low-level details. Both should depend on abstractions. A notification use case can depend on a Notifier interface, with email, SMS, and test implementations supplied externally. 6. Reusable Components and InterfacesA reusable component has a clear purpose, stable input and output contracts, manageable dependencies, and predictable error behaviour. Reusability is not achieved merely by making a function generic. A good component contract should state: - accepted input and validation rules;
- returned data and its meaning;
- possible errors and their categories;
- side effects, such as database writes or network calls;
- performance or consistency expectations;
- ownership and versioning responsibilities.
Prefer domain-oriented interfaces. For example, ReserveInventory communicates more meaningfully than a generic updateRow when used by an order workflow. However, infrastructure interfaces may appropriately reflect technical operations when their purpose is persistence or messaging. Error handling should preserve useful context without exposing secrets. External errors should be translated into stable application errors. A database exception should not normally be returned directly to a client because it may reveal schema details and does not provide a suitable user-facing explanation. 7. Transactions and ConsistencyA transaction groups operations so that they obey a defined consistency boundary. The traditional ACID properties are: - Atomicity: all operations in the transaction succeed, or none are committed.
- Consistency: committed data satisfies defined constraints and invariants.
- Isolation: concurrent transactions do not observe prohibited intermediate effects.
- Durability: committed data survives an accepted system failure, subject to the database’s guarantees.
Suppose placing an order creates an order record and decreases inventory. If the order is created but inventory is not reduced, the system may sell more items than are available. If both operations are in one database transaction, an error can roll them back together. A simplified transaction structure is: begin transaction
validate current state
create order
decrease inventory
commit transaction If an error occurs before commit, the system should roll back. A transaction does not automatically solve every concurrency problem. The application may need row locks, optimistic version checks, unique constraints, or an appropriate isolation level. Transactions should usually be short. Holding a database transaction open while waiting for an email provider or payment gateway can increase lock contention and reduce availability. A common approach is to commit the local state first and then publish an event using an outbox pattern. The event is stored in the same transaction as the business change; a separate worker sends it to the message system. Distributed transactions across several independent systems are difficult and often expensive. Alternatives include sagas, compensating actions, retries, and explicit intermediate states. A compensation is not a perfect rollback: a sent email cannot always be unsent, and a payment reversal may take time. 8. Idempotency and Reliable RetriesAn operation is idempotent if repeating it produces the same intended result as performing it once. In mathematical notation, an operation f is idempotent when: f(f(x)) = f(x) Here, x is the system state and f is the operation. The notation describes the desired effect, not necessarily identical response timing or logging. HTTP PUT is designed to be idempotent when replacing a resource with the same representation. A payment request, by contrast, must be designed carefully because blindly retrying it could charge a customer twice. A common practical technique is an idempotency key. The client sends a unique key for a logical operation. The server stores the key, request identity, and result. If the same key is received again, the server returns the original result rather than creating a second resource. The stored record should be protected by a uniqueness constraint so concurrent requests cannot both pass a preliminary check. Idempotency keys require policy decisions: - How long are keys retained?
- What happens if the same key is reused with different request data?
- Is the key scoped to a user, account, or endpoint?
- Which failures are safe to retry?
Idempotency is not the same as deduplication in every context. A message consumer may record processed message identifiers, while an API may store the result of a client request. Both address duplicate effects but have different lifecycles and failure modes. 9. Worked Example: Designing an Order Use CaseConsider an online stationery store. The requirement is: a customer can place an order if the products exist, sufficient stock is available, and the customer account is active. The total is calculated from item prices and quantities, and inventory must be reduced only when the order is successfully created. A poor implementation might put all of the following in the controller: parse JSON, query products, calculate totals, update stock, insert the order, and send email. This implementation is difficult to test and risks partial updates. A maintainable design separates the work: - Controller: validates request shape, obtains the authenticated customer, and calls
PlaceOrder. - Application service: starts the transaction, loads required data, invokes domain behaviour, saves the result, and returns an outcome.
- Domain model: checks account status, product availability, quantity rules, and order invariants.
- Repositories: load and save customers, products, and orders.
- Outbox component: records an
OrderPlaced event in the same transaction. - Worker: sends confirmation email after the transaction commits.
A simplified service interface could be: PlaceOrder(command, idempotencyKey) → OrderResult The command contains customer identity and requested items. The service should not trust a total supplied by the client; it calculates the total using authoritative product prices. It should also avoid loading an entire database table when only the requested product identifiers are needed. Concurrency remains important. Two customers may attempt to buy the last item simultaneously. The database may enforce a stock constraint, use a conditional update, or use optimistic locking with a version number. The correct choice depends on the database and expected workload, but the invariant must be protected at the persistence boundary. 10. Illustrative Case Study: Community Clinic Booking SystemBackgroundThis illustrative case concerns a small community clinic that allows patients to book appointments through a web application. Staff can create schedules, patients can request appointments, and the system sends reminders. ProblemThe first version placed booking rules in the HTTP controller. It checked whether a time was free, inserted an appointment, and then sent an email. Under concurrent requests, two patients occasionally received the same time slot. When email delivery failed, the controller returned an error even though the appointment had already been inserted, encouraging patients to retry and creating confusion. AnalysisThe system had three architectural problems: - business rules were coupled to HTTP;
- availability checking and appointment creation were not protected by a suitable transaction or uniqueness rule;
- notification failure was treated as if booking itself had failed.
There was also a privacy concern: error messages included unnecessary patient details in application logs. Approach and solutionThe team introduced a BookAppointment application service. The service: - validates the requested appointment type and patient permissions;
- checks schedule rules in the domain layer;
- creates the appointment within a database transaction;
- protects the time-slot invariant using a database uniqueness constraint and appropriate concurrency handling;
- stores an appointment-created event in an outbox table;
- returns a successful booking response after commit.
The reminder worker reads the outbox and sends email or SMS. Failed deliveries are retried with a bounded policy. The booking API uses an idempotency key so a client retry returns the existing appointment rather than creating another one. OutcomeThe system now distinguishes booking failure from notification delay. Concurrent attempts cannot create duplicate appointments, and the booking rule can be tested without an HTTP server. Logs use appointment identifiers rather than unnecessary medical details, supporting privacy and operational diagnosis. Learning pointsThe case demonstrates that architecture is not merely folder arrangement. It protects business invariants, clarifies failure behaviour, supports retries, and reduces exposure of sensitive information. It also shows that a transaction should cover the business state that must change together, while external notifications can be handled asynchronously. 11. Testing Maintainable Business LogicArchitecture should make important behaviour easy to test. Useful testing levels include: - Unit tests: test value objects, entities, policies, and services with fast in-memory dependencies.
- Integration tests: verify repositories, database constraints, transactions, and message adapters against real or controlled infrastructure.
- End-to-end tests: verify a realistic request through the system, but use them selectively because they are slower and more fragile.
Tests should focus on behaviour rather than private implementation details. For an order service, useful cases include insufficient stock, inactive customer, duplicate idempotency key, concurrent booking, and failure during notification. Database constraints should also be tested because an application-only check may not protect against concurrent writes. Observability supports maintainability in production. Structured logs, correlation identifiers, metrics, and traces help locate failures across controllers, services, repositories, and workers. Sensitive data should be minimized, masked, or excluded. Operational convenience does not justify exposing passwords, payment credentials, or unnecessary health information. 12. Architectural Trade-offs and Best PracticesNo architecture is universally best. A layered monolith is often an excellent starting point because it is simple to deploy and can still have strong internal boundaries. Microservices introduce independent deployment and scaling, but also network failures, distributed tracing, deployment complexity, data ownership questions, and operational cost. Splitting a system into services before boundaries are understood can make change harder rather than easier. A practical decision process asks: - How complex and volatile are the business rules?
- How many developers and teams will maintain the system?
- Are independent deployment and scaling genuinely required?
- What consistency and availability guarantees are needed?
- What are the security, privacy, and regulatory obligations?
- Can the team operate the chosen infrastructure reliably?
Best practices include: - Keep controllers thin and use-case-oriented.
- Keep domain rules independent of frameworks where practical.
- Define transaction boundaries explicitly.
- Enforce critical invariants in the database as well as application code.
- Use idempotency for operations likely to be retried.
- Prefer clear names and small, cohesive modules over clever abstractions.
- Separate external side effects from core state changes when appropriate.
- Document important assumptions, failure modes, and ownership.
- Introduce abstractions when they reduce coupling, not merely because a pattern exists.
- Refactor incrementally; maintainability is developed continuously.
Security and governance must be architectural concerns. Authorization should be checked at the use-case boundary, not only by hiding a button in the interface. Audit records may be necessary for sensitive actions. Personal data should be collected and retained only for legitimate purposes, protected in transit and at rest, and removed according to applicable policy or law. Automated decision rules should be reviewable, explainable where required, and monitored for unfair outcomes. Contemporary backend systems increasingly use asynchronous messaging, serverless functions, managed databases, and AI-enabled services. These technologies do not remove the need for business boundaries. An AI service may classify support requests, but the application still needs validation, authorization, timeout handling, privacy controls, and a defined response when the model is unavailable or incorrect. External services should be treated as fallible adapters, not as trusted domain authorities. Chapter SummaryMaintainable backend architecture separates business rules from delivery mechanisms and technical infrastructure. Layered architecture distinguishes presentation, application, domain, and infrastructure responsibilities. MVC helps organize request handling, while service-repository design gives use cases a clear place to coordinate workflows and repositories a clear persistence role. Modular design depends on cohesion, low coupling, encapsulation, and explicit interfaces. Entities protect identity-based rules, value objects express validated concepts, and invariants preserve correctness. Clean architecture and dependency inversion keep core policies independent from frameworks and external vendors. SOLID principles provide practical guidance for controlling change, although they must be applied proportionately. Transactions protect changes that must succeed together. Idempotency makes retries safer, especially in APIs and asynchronous systems. Outbox patterns, concurrency controls, database constraints, and careful error handling address failures that simple request-response code often overlooks. Finally, architecture involves trade-offs: a well-structured monolith may be preferable to premature microservices, and every design must consider security, privacy, operational capability, and the actual needs of the system. Key TermsBusiness logic: Rules and decisions that express what an application does for its users or organization. Layered architecture: An arrangement that separates responsibilities into levels with controlled dependencies. MVC: Model–View–Controller, a pattern for separating data or behaviour, representation, and request coordination. Controller: A component that translates an external request into an application operation and formats the result. Application service: A component that coordinates a particular use case or workflow. Domain model: Entities, value objects, policies, and rules representing the problem domain. Repository: An abstraction for loading and storing domain-related data. Module: A cohesive unit of code with an explicit boundary and interface. Cohesion: The degree to which the responsibilities of a component belong together. Coupling: The degree of dependency between components. Entity: An object whose identity distinguishes it over time. Value object: An object defined by its attributes rather than a persistent identity. Invariant: A condition that must remain true for valid system state. Dependency inversion: A principle in which high-level policy depends on abstractions rather than technical details. SOLID: Five principles for designing maintainable object-oriented software. Transaction: A group of operations managed as a consistency unit. ACID: Atomicity, consistency, isolation, and durability properties of transactions. Idempotency: The property that repeating an operation produces the same intended effect as performing it once. Outbox pattern: A method of storing an event with a business change and publishing it later. Clean architecture: An approach that keeps core business policies independent of external frameworks and infrastructure. Review QuestionsShort Answer Questions- What is the difference between business logic and infrastructure logic?
- What responsibilities should normally belong to an application service?
- Define cohesion and coupling.
- What does idempotency mean in backend systems?
- Name and briefly explain the four ACID properties.
Long Answer Questions- Explain how layered architecture, MVC, and service-repository design can work together in a backend application.
- Discuss the SOLID principles and explain why applying them mechanically can create unnecessary complexity.
- Explain how transactions and database constraints can work together to protect business invariants.
- Compare a well-structured monolith with a microservice architecture, including benefits and costs.
- Describe how clean architecture applies dependency inversion to an external payment provider.
Analytical / Application Questions- An API creates a customer account and sends a welcome email. A network timeout occurs after the account is committed but before the response reaches the client. Design a retry-safe solution.
- Two patients request the last available appointment slot at the same time. Identify the invariant and propose application-level and database-level protections.
- A controller contains validation, pricing calculations, SQL queries, payment calls, and email delivery. Analyze the design using at least three SOLID principles and propose a refactoring plan.
Multiple Choice Questions with AnswersQuestion 1Which component should normally coordinate a use case such as placing an order? A. The application service B. The database driver C. The HTML template D. The network socket Correct Answer: A. The application service Question 2Which statement best describes an invariant? A. A condition that must remain true for valid system state B. A temporary log message C. A user-interface colour choice D. A database connection string Correct Answer: A. A condition that must remain true for valid system state Question 3Which SOLID principle says that high-level policy should not depend directly on low-level technical details? A. Single Responsibility Principle B. Liskov Substitution Principle C. Dependency Inversion Principle D. Interface Segregation Principle Correct Answer: C. Dependency Inversion Principle Question 4What is the main purpose of a repository in service-repository architecture? A. To format HTML pages B. To hide persistence details behind an interface C. To authenticate every network packet D. To replace all domain rules Correct Answer: B. To hide persistence details behind an interface Question 5Which ACID property means that a transaction’s changes are all committed or all rolled back? A. Atomicity B. Isolation C. Durability D. Availability Correct Answer: A. Atomicity Question 6Which technique helps prevent a client retry from creating two orders? A. An idempotency key B. A larger HTML form C. A second controller with the same code D. Removing database constraints Correct Answer: A. An idempotency key Question 7Why should a long external email call generally not be held inside a database transaction? A. It can increase lock duration and contention B. Email calls always modify database rows C. Transactions cannot contain any application logic D. It guarantees duplicate emails Correct Answer: A. It can increase lock duration and contention Question 8Which characteristic describes high cohesion? A. A module contains closely related responsibilities B. A module depends on every other module C. A module exposes all internal variables D. A module mixes unrelated business areas Correct Answer: A. A module contains closely related responsibilities Question 9What is the main role of an outbox pattern? A. To store an event with a business change before later publication B. To replace all database transactions with email C. To encrypt source code files D. To render API documentation Correct Answer: A. To store an event with a business change before later publication Question 10Which architecture choice is often appropriate for a small application with limited operational capacity? A. A well-structured monolith B. An arbitrarily large microservice mesh C. A system with no persistence boundary D. A separate service for every function Correct Answer: A. A well-structured monolith
|