Backend Architecture and Code QualityIntroductionA backend application is more than a collection of route handlers. It is a system that receives requests, applies business rules, manages data, enforces security, communicates with external services, and returns reliable responses. As an application grows, placing all of these responsibilities in a few controllers or files produces duplication, difficult testing, unpredictable change, and fragile deployments. Backend architecture provides a structure for controlling complexity. In a Node.js and Express application, architecture determines where HTTP concerns end, where business decisions are made, how persistence is accessed, and how modules communicate. Code quality concerns whether this structure remains understandable, testable, secure, observable, and adaptable. This chapter examines layered architecture, MVC, service-repository design, dependency separation, and the SOLID principles. It also compares a modular monolith with microservices and explains why architectural decisions should be based on system needs rather than fashion. Learning ObjectivesBy the end of this chapter, you should be able to: - explain the purpose of backend architecture and separation of concerns;
- distinguish MVC, layered architecture, and service-repository patterns;
- organize an Express application into coherent modules;
- apply dependency inversion and other SOLID principles in Node.js;
- evaluate code quality using maintainability, testability, and operational criteria;
- compare modular monoliths and microservices;
- identify architectural risks and propose an appropriate design for a realistic application.
1. Architecture as a Response to ComplexitySoftware architecture is the set of significant design decisions that shape a system’s structure, behavior, and evolution. It includes module boundaries, data ownership, communication mechanisms, deployment units, security boundaries, and technology choices. A small application may work with a simple structure: server.js
routes.js
models.js This is not automatically poor design. For a small, stable application, a simple structure can be efficient. Problems arise when unrelated concerns become tightly coupled. For example, a route handler may validate input, calculate prices, query a database, send an email, and format an HTTP response. Such a handler is difficult to test because every test requires HTTP and database setup. A useful architecture separates responsibilities while preserving a clear flow of control. Separation does not mean creating a file for every function. It means that a change in one concern should have a limited and predictable effect on other concerns. Important quality attributes include: | Quality attribute | Meaning in a backend system |
|---|
| Maintainability | The ease with which developers can understand and change the system | | Testability | The ability to verify behavior in isolation and at system boundaries | | Reliability | The ability to perform correctly under expected and exceptional conditions | | Scalability | The ability to handle increased load through appropriate resource changes | | Security | Protection of data, operations, identities, and infrastructure | | Observability | The ability to understand system behavior through logs, metrics, and traces | | Deployability | The ease and safety of releasing changes |
These attributes may conflict. A highly distributed architecture may permit independent deployment but introduce network failures and operational complexity. A highly abstract design may improve testability but become difficult for a small team to understand. Architecture is therefore a set of trade-offs, not a search for one universally correct pattern. 2. Separation of Concerns and Layered ArchitectureSeparation of concerns divides a system according to distinct responsibilities. In a common Express backend, the following layers are useful: - Presentation or interface layer: HTTP routes, controllers, request parsing, response formatting, and middleware.
- Application or service layer: use cases and business workflows.
- Domain layer: business concepts, rules, and invariants.
- Infrastructure or repository layer: databases, message brokers, file systems, and external APIs.
A typical request flows inward and a result flows outward: HTTP request → route → controller → service → repository → database
HTTP response ← controller ← service ← repository ← database The controller should not need to know SQL details. The repository should not decide HTTP status codes. The service should not depend unnecessarily on Express’s Request and Response objects. 2.1 Presentation layerThe presentation layer translates between an external protocol and application operations. An Express controller commonly performs four tasks: - reads route parameters, query parameters, and request bodies;
- delegates work to an application service;
- translates known outcomes into HTTP responses;
- passes unexpected errors to centralized error middleware.
A controller should remain thin. For example: export function createOrderController(orderService) {
return async function createOrder(req, res, next) {
try {
const order = await orderService.createOrder({
customerId: req.user.id,
items: req.body.items
});
res.status(201).json({ data: order });
} catch (error) {
next(error);
}
};
}The controller does not calculate totals or directly call the database. It adapts HTTP input to a service operation. 2.2 Service layerA service represents an application use case. It coordinates business actions and may call multiple repositories or other services. For example, an order service might validate product availability, calculate a total, reserve stock, create an order, and publish an event. Services should contain business decisions rather than merely forwarding every method to a repository. A service with no meaningful behavior may indicate that the design is unnecessarily fragmented; a service containing every operation in the system may indicate that responsibilities have not been separated adequately. 2.3 Domain layerThe domain layer expresses rules that should remain valid regardless of whether the system is accessed through HTTP, a command-line tool, or a message consumer. A domain rule might state that an order cannot be confirmed when it contains no items. For simple applications, domain logic can reside in services. For complex domains, entities, value objects, domain services, and domain events may provide clearer models. The appropriate level of modeling depends on business complexity. 2.4 Repository and infrastructure layerA repository provides an application-oriented interface to stored data. It hides persistence details such as SQL, MongoDB queries, connection management, and transaction mechanics. export function createUserRepository(db) {
return {
async findByEmail(email) {
return db.users.findOne({ email });
},
async insert(user) {
return db.users.insertOne(user);
}
};
}A repository is not necessarily a generic wrapper around every database method. It should expose operations meaningful to the application and preserve important data-access rules, such as filtering inactive records or applying transactions. 3. MVC and Service-Repository ArchitectureModel–View–Controller (MVC) separates an application into three broad responsibilities: - Model: data and domain behavior;
- View: presentation of results;
- Controller: coordination of input and output.
In an Express API, the view is usually a JSON representation rather than an HTML template. Consequently, MVC is often combined with services and repositories: Router → Controller → Service → Repository → Database
↓
DomainMVC is a conceptual classification, while layered architecture is a broader organizational approach. The terms overlap but are not identical. A model class that contains database queries, validation, and business rules may be called a model in an MVC application, but it may violate a desired separation between domain and infrastructure. A practical module structure may look like this: src/
app.js
server.js
config/
environment.js
modules/
orders/
order.routes.js
order.controller.js
order.service.js
order.repository.js
order.schema.js
order.errors.js
users/
user.routes.js
user.controller.js
user.service.js
user.repository.js
infrastructure/
database.js
logger.js
middleware/
error-handler.js
authentication.jsThis structure combines layers with feature-based modules. Related files for a business capability are kept together, while each file still has a clear responsibility. 4. Dependency Separation and Dependency InjectionA dependency is anything a component needs to perform its work: a database client, clock, logger, email sender, payment gateway, or configuration object. Hard-coded dependencies make tests slow and make replacement difficult. Dependency injection supplies dependencies from outside a component rather than constructing them internally. Poor separation: class InvoiceService {
constructor() {
this.db = new PostgresClient(process.env.DATABASE_URL);
}
}Improved separation: class InvoiceService {
constructor(invoiceRepository, paymentGateway, clock) {
this.invoiceRepository = invoiceRepository;
this.paymentGateway = paymentGateway;
this.clock = clock;
}
}The second design permits a test to provide a fake repository, a payment gateway stub, and a fixed clock. It also avoids coupling the service to one database driver. A composition root is the place where concrete implementations are assembled. In an Express application, it may be app.js: const repository = createInvoiceRepository(db);
const gateway = createPaymentGateway(config.paymentUrl);
const clock = { now: () => new Date() };
const service = new InvoiceService(repository, gateway, clock);
const controller = createInvoiceController(service);The application’s core code depends on interfaces or behavioral contracts, while the composition root selects implementations. JavaScript does not require formal interfaces, but a documented contract and consistent method shape can serve a similar purpose. TypeScript interfaces can make such contracts explicit, but they do not replace sound design. 5. SOLID Principles in Node.jsSOLID is a group of design principles. They are guidelines, not laws. Applying them mechanically can create excessive abstraction, so each principle should be evaluated against the system’s complexity and likely change. 5.1 Single Responsibility PrincipleThe Single Responsibility Principle states that a module should have one coherent reason to change. A controller responsible for validation, pricing, persistence, email, and response formatting has several reasons to change. A useful interpretation is not “one function per file,” but “one cohesive responsibility per module.” A user registration service may coordinate registration, while an email adapter handles email delivery and a repository handles persistence. 5.2 Open–Closed PrincipleThe Open–Closed Principle says that software entities should be open for extension but closed for modification. In practice, a notification service can depend on a sender contract and support email, SMS, or a test sender without changing its core workflow. This principle is not a requirement to predict every future feature. Premature plugin systems can be more complex than a direct implementation. Extension points should be introduced where variation is real and likely. 5.3 Liskov Substitution PrincipleThe Liskov Substitution Principle requires that an implementation can replace an abstraction without violating the expectations of its users. If a repository contract promises findById to return either an entity or null, an implementation that unexpectedly throws for every missing record is not substitutable unless that behavior is part of the contract. Contracts include return types, error behavior, side effects, and performance expectations where these affect correctness. 5.4 Interface Segregation PrincipleThe Interface Segregation Principle advises that clients should not depend on methods they do not use. Instead of injecting a large DatabaseService with dozens of operations, a reporting service may receive only a salesReader with the required query methods. Small interfaces reduce accidental coupling and make test doubles easier to write. 5.5 Dependency Inversion PrincipleThe Dependency Inversion Principle has two related ideas: - high-level policy should not depend directly on low-level details;
- both should depend on abstractions.
An order service should depend on an order repository contract, not on a specific MongoDB collection. The database adapter then implements the contract. This permits the business workflow to be tested without a live database. 6. Designing Error Handling and BoundariesReliable architecture defines how errors move through the system. Errors can be classified as: - input errors, such as malformed or missing fields;
- business errors, such as attempting to cancel an already completed order;
- infrastructure errors, such as database unavailability;
- programming errors, such as accessing an undefined property unexpectedly.
These categories should not all produce the same response. A validation error may produce HTTP 400, an authentication failure HTTP 401, an authorization failure HTTP 403, a missing resource HTTP 404, and an unexpected infrastructure failure HTTP 500. The exact mapping should follow the API contract and security requirements. Centralized error middleware helps maintain consistent responses: export function errorHandler(error, req, res, next) {
if (error.name === 'ValidationError') {
return res.status(400).json({ error: 'Invalid request' });
}
req.log?.error({ error }, 'Unhandled request error');
return res.status(500).json({ error: 'Internal server error' });
}Public responses should not expose stack traces, SQL statements, access tokens, or internal infrastructure details. Detailed information belongs in protected logs, subject to privacy and retention policies. Transactions and consistency boundaries also matter. If an operation updates several related records, the service should define whether all changes must succeed together. A repository may expose a transaction callback or unit-of-work abstraction. Distributed transactions across independent services are substantially more difficult and should not be assumed to behave like a local database transaction. 7. Code Quality as an Engineering PracticeCode quality is multidimensional. Formatting and naming are valuable, but quality also includes correctness, security, observability, and ease of change. 7.1 Readability and namingNames should describe intent. calculatePayableTotal is more informative than processData. Functions should be small enough to understand, but not artificially split into meaningless fragments. Comments should explain a non-obvious decision or constraint rather than restate the code. 7.2 Cohesion and couplingCohesion measures how closely related the responsibilities within a module are. Coupling describes how strongly modules depend on one another. High cohesion and low unnecessary coupling generally improve maintainability. Coupling is not always bad. A service necessarily depends on a repository if it needs data. The design goal is explicit, stable, and meaningful coupling rather than hidden dependencies and circular imports. 7.3 Testing strategyA maintainable backend normally uses several testing levels: - unit tests for domain functions and services using test doubles;
- integration tests for repositories and database behavior;
- API tests for routes, middleware, authentication, and response contracts;
- end-to-end tests for selected business journeys.
Mocking every dependency can produce tests that pass while the real integration is broken. Conversely, using a full database for every unit test makes feedback slow. A balanced test strategy tests each boundary at the level where its behavior is meaningful. 7.4 Static quality controlsProfessional projects commonly automate: - formatting with a consistent tool;
- linting for suspicious or inconsistent code;
- type checking where TypeScript is used;
- dependency vulnerability scanning;
- automated tests and coverage reporting;
- continuous integration checks.
Coverage is a signal, not proof of quality. A test suite can execute many lines without checking important outcomes. Branch behavior, failure paths, authorization, and data integrity deserve attention. 7.5 Configuration and secretsConfiguration should be externalized by environment. Secrets must not be committed to source control or printed in logs. Configuration loading should validate required values at startup so that a deployment fails clearly rather than behaving unpredictably during a request. 8. Modular Monoliths and MicroservicesA monolith is deployed as one application unit. A modular monolith is also deployed as one unit, but its internal modules have explicit boundaries, ownership, and communication rules. A microservice architecture decomposes the system into independently deployable services, each typically owning a business capability and its data. 8.1 Modular monolithA modular monolith offers: - simple local development and deployment;
- in-process function calls rather than network calls;
- straightforward transactions within one database;
- relatively low operational overhead;
- an easier starting point for a small or medium team.
Its risks include accidental coupling between modules, a shared database that undermines ownership, and deployments in which a small change requires rebuilding the entire application. 8.2 MicroservicesMicroservices can support independent scaling, deployment, and technology choices. They may be appropriate when teams have clear ownership boundaries, parts of the system require different scaling profiles, or organizational and regulatory needs demand isolation. They also introduce: - network latency and partial failure;
- service discovery and configuration management;
- distributed tracing and log correlation;
- versioned contracts and compatibility concerns;
- more complicated testing and deployment;
- data consistency challenges;
- higher operational and security costs.
A function call inside a modular monolith is not equivalent to an HTTP call between services. A network call can time out, be retried, be duplicated, or succeed remotely while the caller fails before receiving the response. Designs must account for idempotency, timeouts, retry limits, circuit breaking, and observability. 8.3 Comparison| Criterion | Modular monolith | Microservices |
|---|
| Deployment | One deployable unit | Multiple independently deployed units | | Communication | In-process calls | Network or messaging calls | | Data transactions | Easier within one database | Often requires coordination or eventual consistency | | Operations | Lower complexity | Higher complexity | | Team ownership | Shared or module-based | Strong service ownership expected | | Scaling | Usually scales as an application | Services can scale independently | | Failure modes | Mainly process and dependency failures | Adds network and distributed-system failures | | Best fit | Many small and medium systems | Large systems with justified boundaries and mature operations |
The recommended progression for many teams is a well-structured modular monolith. A module can later be extracted when its boundary, data ownership, traffic pattern, and operational justification are understood. Extraction should be driven by evidence, not by the assumption that distributed architecture is automatically more advanced. 9. Illustrative Case: A Learning Marketplace BackendBackgroundConsider an illustrative application called CourseMarket. Students browse courses, enroll, pay, and receive access. Instructors publish courses and view enrollment reports. ProblemThe initial Express application contains large route handlers. The enrollment route validates payment data, queries courses, inserts enrollment records, sends email, and formats responses. A change to payment behavior requires modifying several routes. Tests are slow because every test starts a database and calls external payment APIs. AnalysisThe application has several distinct responsibilities: - HTTP authentication and request validation;
- enrollment business rules;
- course and enrollment persistence;
- payment gateway communication;
- email notification;
- reporting queries.
The main risks are tight coupling, duplicated rules, hidden side effects, and unclear transaction behavior. For example, if payment succeeds but enrollment insertion fails, the system must define how the failure is recovered. ApproachThe team chooses a modular monolith with courses, enrollments, payments, and notifications modules. The enrollment controller accepts a validated request and calls enrollmentService.enrollStudent. The service depends on courseRepository, enrollmentRepository, paymentGateway, and eventPublisher abstractions. The local database transaction creates the enrollment and records payment status where appropriate. A durable event or outbox record is used to trigger email after the transaction commits. The email sender is not called directly inside the transaction, because an email failure should not necessarily roll back a successful enrollment. Tests include: - unit tests for rules such as duplicate enrollment prevention;
- repository integration tests against a test database;
- contract tests for the payment adapter;
- API tests for authentication and status codes;
- one end-to-end test covering the student enrollment journey.
OutcomeThe application remains one deployable Node.js service, but its modules have explicit responsibilities. Payment and email implementations can be replaced in tests. Controllers are short, business rules are centralized, and failures can be observed through structured logs and correlation identifiers. Learning pointsThe case demonstrates that architecture is not synonymous with microservices. A modular monolith can provide strong boundaries and professional maintainability. It also demonstrates that asynchronous events should be used deliberately, with attention to delivery guarantees, duplicate messages, and recovery behavior. 10. Governance, Security, and Contemporary PracticeArchitecture has governance consequences. Access control should be enforced at the service or domain boundary, not only by hiding a route. Authorization rules must be tested for both permitted and forbidden actions. Sensitive data should be minimized, encrypted where appropriate, and retained only as long as justified. Dependency management is also a governance issue. Node.js applications depend on many open-source packages, so teams should review licenses, monitor vulnerabilities, lock dependency versions appropriately, and remove unused packages. Supply-chain attacks make package provenance and update processes important operational concerns. Artificial intelligence tools can assist with scaffolding, test generation, code explanation, and defect discovery. However, generated code must be reviewed for insecure authorization, leaked secrets, incorrect error handling, license concerns, and unsuitable abstractions. AI output does not replace code ownership, testing, or architectural reasoning. Contemporary backend practice increasingly emphasizes: - structured logs with request or trace identifiers;
- health and readiness endpoints;
- metrics for latency, errors, throughput, and resource use;
- distributed tracing where services communicate over networks;
- automated delivery with rollback mechanisms;
- contract testing between independently evolving components;
- secure defaults and continuous dependency monitoring.
11. A Practical Architecture Review MethodWhen reviewing an Express backend, ask the following questions: - Can each major business capability be located and understood?
- Are controllers concerned primarily with transport and response translation?
- Are business rules centralized rather than duplicated across routes?
- Can services be tested without a live database or external provider?
- Are repositories meaningful application boundaries rather than indiscriminate database wrappers?
- Are errors classified, logged safely, and mapped consistently?
- Are module dependencies directed and free from unnecessary cycles?
- Does each module have clear ownership of its data and behavior?
- Is the chosen deployment architecture justified by team capacity and operational needs?
- Can the system be observed and safely changed?
An architecture review should examine real change scenarios. Ask what happens when the payment provider changes, when a new response format is required, when the database is unavailable, or when two teams modify related functionality. The best architecture is often revealed by how safely it accommodates these changes. Challenges and LimitationsLayered architecture can become bureaucratic if every trivial operation requires multiple interfaces and classes. Repositories can obscure useful database capabilities if their abstractions are too generic. Dependency injection can be overused, producing complicated constructors and assembly code. SOLID principles may conflict with simplicity when applied without regard to context. Modular monoliths can gradually decay if module boundaries are not enforced. Microservices can fail through distributed complexity even when each individual service is well written. No pattern guarantees quality: disciplined naming, testing, monitoring, secure operation, and thoughtful change management remain necessary. The central design question is therefore not “Which pattern is fashionable?” but “Which structure makes the required behavior clear, testable, secure, and economical to operate?” Chapter SummaryBackend architecture organizes responsibilities so that HTTP transport, business workflows, domain rules, and infrastructure details do not become unnecessarily entangled. Layered architecture commonly separates presentation, application services, domain logic, and repositories or infrastructure. MVC provides a broad model for separating input coordination, application data, and presentation; in Express APIs it is often combined with service-repository modules. Dependency injection and dependency inversion allow high-level business logic to remain independent of databases, payment providers, clocks, and other infrastructure. SOLID principles provide useful guidance for cohesion, extension, substitutability, focused interfaces, and stable dependencies, but they should be applied pragmatically. Code quality includes readability, testing, error handling, security, observability, configuration discipline, and manageable coupling. A modular monolith often provides a strong balance between internal structure and operational simplicity. Microservices may be justified by independent scaling, ownership, or deployment needs, but introduce network, consistency, and operational challenges. Architecture should therefore be selected according to evidence, team capability, and business requirements. Key TermsArchitecture: Significant structural decisions that shape a software system and its evolution. Separation of concerns: Division of a system into responsibilities that can be understood and changed independently. Layered architecture: Organization into layers such as presentation, application, domain, and infrastructure. MVC: Model–View–Controller pattern for separating data, presentation, and input coordination. Controller: Component that translates external requests into application operations and results into responses. Service: Component that coordinates an application use case or business workflow. Domain: The business concepts, rules, and invariants relevant to the system. Repository: Application-oriented abstraction for accessing and storing data. Dependency injection: Supplying a component’s dependencies from outside the component. Composition root: Location where concrete implementations are assembled. SOLID: Five principles guiding maintainable object and module design. Cohesion: Degree to which responsibilities within a module belong together. Coupling: Degree of dependency between modules. Idempotency: Property whereby repeating an operation produces the same intended result as performing it once. Modular monolith: One deployable application containing internally separated modules. Microservice: Independently deployable service organized around a business capability. Observability: Ability to understand system behavior through logs, metrics, and traces. Technical debt: Future cost created by expedient or poorly controlled technical decisions. Composition root: The application location that wires abstractions to implementations. Contract testing: Testing that verifies compatibility between communicating components. Review QuestionsShort Answer Questions- What is the main purpose of separating controllers from services and repositories?
- Define dependency injection and give one Node.js example of a dependency.
- State the Dependency Inversion Principle in your own words.
- What is a modular monolith?
- Why should public error responses avoid exposing stack traces?
Long Answer Questions- Explain how a request flows through a layered Express architecture. Describe the responsibility of each layer.
- Compare MVC with service-repository architecture and explain how they can be used together.
- Discuss all five SOLID principles with practical examples from backend development.
- Compare the operational and technical trade-offs of a modular monolith and microservices.
- Explain how testing, logging, configuration, and dependency management contribute to backend code quality.
Analytical / Application Questions- An Express controller validates input, calculates an order total, updates three database tables, sends an email, and returns a response. Identify the design problems and propose a refactored module structure.
- A payment request succeeds at the provider but the local enrollment transaction fails. Analyze the consistency problem and propose an approach involving idempotency, recovery, or durable events.
- A growing application has four development teams but a single shared database and frequent deployment conflicts. Evaluate whether it should immediately adopt microservices, and justify a staged architectural recommendation.
Multiple Choice Questions with AnswersQuestion 1Which responsibility most appropriately belongs to an Express controller? A. Writing database-specific queries B. Translating an HTTP request into a service call C. Defining the organization’s pricing policy D. Managing database connection pooling Correct Answer: B. Translating an HTTP request into a service call Question 2What is the primary purpose of a repository in a layered backend? A. To format HTTP responses B. To replace all domain rules C. To provide an application-oriented data-access boundary D. To configure the operating system Correct Answer: C. To provide an application-oriented data-access boundary Question 3Which SOLID principle states that high-level policy should not depend directly on low-level implementation details? A. Single Responsibility Principle B. Open–Closed Principle C. Interface Segregation Principle D. Dependency Inversion Principle Correct Answer: D. Dependency Inversion Principle Question 4Which design most directly improves a service’s testability? A. Constructing the database client inside the service B. Injecting a repository and payment gateway into the service C. Putting all operations in one route handler D. Using global mutable configuration Correct Answer: B. Injecting a repository and payment gateway into the service Question 5Which statement best describes a modular monolith? A. Several services that must communicate over a network B. One deployable application with explicit internal module boundaries C. A system with no separation of concerns D. A database that contains multiple schemas Correct Answer: B. One deployable application with explicit internal module boundaries Question 6Why can microservices make transactions more difficult? A. They eliminate all data storage B. They require every operation to be synchronous C. They introduce network boundaries and may distribute data across services D. They prevent services from having APIs Correct Answer: C. They introduce network boundaries and may distribute data across services Question 7Which quality attribute concerns the ability to understand system behavior through logs, metrics, and traces? A. Observability B. Cohesion C. Portability D. Compilation Correct Answer: A. Observability Question 8Which action best follows the Single Responsibility Principle? A. Making one controller handle validation, payment, email, and persistence B. Putting every database operation in a single global class C. Separating email delivery from enrollment business logic D. Allowing each route to implement its own pricing rules Correct Answer: C. Separating email delivery from enrollment business logic Question 9What is a major risk of using an HTTP call between services instead of an in-process function call? A. The call cannot return data B. The call may experience timeout, duplication, or partial failure C. The call always requires a relational database D. The call makes testing impossible Correct Answer: B. The call may experience timeout, duplication, or partial failure Question 10Which is the most appropriate general recommendation for a small team beginning a complex backend? A. Start with many microservices regardless of requirements B. Use a well-structured modular monolith and extract services when justified C. Avoid all module boundaries until scaling occurs D. Place all business logic in route handlers Correct Answer: B. Use a well-structured modular monolith and extract services when justified
|