Advanced Backend Development (Node.js, Django, Spring Boot)

Chapter 2
Language, Framework and Project Architecture

Post Graduate Diploma in Full Stack Development
Advanced Backend Development (Node.js, Django, Spring Boot)   •   Post Graduate

2. Language, Framework and Project Architecture

Introduction and context

Backend development is not simply the production of HTTP endpoints. It is the disciplined construction of software that remains understandable, testable, secure, configurable, and adaptable as requirements change. At postgraduate level, the choice of programming language and framework must therefore be considered together with project architecture. A framework can accelerate development, but it cannot compensate for unclear boundaries, uncontrolled dependencies, poor configuration, or weak ownership of business rules.

This chapter examines three widely used backend ecosystems: TypeScript/JavaScript with Express or NestJS, Python with Django and Django REST Framework, and Java or Kotlin with Spring Boot. These ecosystems differ in syntax, runtime model, conventions, and degree of framework guidance. Nevertheless, they address recurring architectural problems: how modules communicate, how dependencies are supplied, how environments are configured, how packages are managed, and how applications are kept maintainable.

The central argument is that architecture is not a property of a framework alone. It is an arrangement of responsibilities, dependencies, policies, and interfaces. Express may support a highly disciplined architecture or an unstructured collection of route handlers. Django may encourage convention while still allowing business logic to become entangled with views and models. Spring Boot may provide powerful inversion of control while still permitting excessive coupling. Professional development requires understanding these possibilities and making deliberate choices.

Learning objectives

After completing this chapter, a student should be able to:

1. Distinguish a programming language, runtime, framework, library, package manager, and architectural style.

2. Explain modularity, cohesion, coupling, separation of concerns, inversion of control, and dependency injection.

3. Design maintainable backend structures using Express or NestJS, Django and Django REST Framework, and Spring Boot.

4. Apply configuration profiles and environment-specific settings safely.

5. Compare package-management practices across npm, Python tooling, and Maven or Gradle.

6. Select an appropriate architectural approach for a project’s scale, team, risk, and operational context.

7. Identify common structural failures and propose improvements.

Language, runtime, framework and package: distinct architectural layers

A programming language defines syntax, semantics, type systems, and mechanisms for expressing computation. TypeScript extends JavaScript with static type checking and is compiled or transpiled into JavaScript, which executes in environments such as Node.js. Python is dynamically typed and is commonly executed by the CPython interpreter. Java is compiled to bytecode executed by the Java Virtual Machine, while Kotlin compiles to JVM bytecode and interoperates with Java.

A runtime provides the environment in which application code executes. Node.js supplies a JavaScript runtime with an event-driven, non-blocking I/O model. The Python runtime executes Django applications through a web-server interface such as WSGI or ASGI. The JVM manages Java and Kotlin applications and supplies mature facilities for memory management, concurrency, monitoring, and portability.

A framework provides reusable structure and conventions. Express is a relatively minimal Node.js web framework. NestJS adds stronger application structure through modules, controllers, providers, decorators, and dependency injection. Django is an opinionated Python framework containing an object-relational mapper, URL routing, administrative facilities, security features, and an application model. Django REST Framework extends Django for API development. Spring Boot simplifies configuration and deployment of Spring applications, while Spring itself supplies a comprehensive inversion-of-control container and ecosystem.

A library offers functionality that application code explicitly calls. A framework, by contrast, commonly calls application code at defined extension points. This distinction is often described as inversion of control: the framework controls the application lifecycle, while developers provide components that the framework invokes.

Packages are distributable units of reusable code. Package managers resolve, install, update, and sometimes audit these units. npm is commonly used for Node.js projects; pip works with Python packages, usually alongside a virtual environment and a lock or constraints strategy; Maven and Gradle manage Java and Kotlin dependencies and build lifecycles. Package management is therefore part of architecture, not merely a setup task. An uncontrolled dependency graph can create security, reproducibility, licensing, and maintenance problems.

Architectural reasoning: from features to boundaries

A maintainable application begins by identifying responsibilities rather than folders. A typical backend must receive requests, validate input, authenticate users, apply business rules, access data, publish events, and produce responses. If one controller performs all these activities, it becomes difficult to test and change. A more sustainable design establishes boundaries between them.

The most useful principles are cohesion and coupling. Cohesion concerns whether the responsibilities within a module belong together. A user-account module that contains registration, password policy, and account status has stronger cohesion than a generic utilities module containing unrelated operations. Coupling concerns the degree to which one module depends on another. Lower coupling makes replacement and testing easier, although eliminating all coupling is neither possible nor desirable.

Separation of concerns assigns distinct kinds of decisions to appropriate components. A controller should understand transport concerns such as HTTP status codes and request parsing. A service or use-case component should express application behaviour. A repository or data-access adapter should handle persistence details. Domain objects should represent meaningful business concepts and rules where the domain warrants such modelling.

A useful request flow is:

`Request → Middleware/Filter → Controller/View → Use Case/Service → Domain Policy → Repository/External Adapter → Response`

This is not a mandatory formula. Small applications may combine layers, whereas regulated or long-lived systems may require stronger isolation. The architectural test is whether a change in one concern unnecessarily forces changes in unrelated concerns.

Layered, modular and hexagonal perspectives

Layered architecture organizes software by technical responsibility, commonly presentation, application, domain, and infrastructure. It is easy to teach and effective for many business systems, but layers can become passive containers if business decisions remain scattered across them.

Feature-oriented modularity organizes code around capabilities such as orders, payments, identity, or notifications. Each feature may contain its controller, service, validation, persistence interface, and tests. This arrangement usually improves discoverability and supports team ownership. It should not be confused with microservices: a modular monolith may contain many well-defined modules in one deployable process.

Hexagonal architecture, also called ports and adapters architecture, places application and domain logic at the centre. The core defines ports—interfaces describing what it needs or offers—while adapters connect those ports to HTTP, databases, message brokers, or third-party APIs. Dependency direction points inward: infrastructure depends on abstractions defined by the core, rather than the core depending directly on a database client.

Clean architecture expresses a similar idea through concentric boundaries. Its value lies not in a prescribed folder tree but in protecting policies from mechanisms. A project should not adopt elaborate layers mechanically. Every boundary introduces communication and mapping costs. The appropriate level of abstraction depends on volatility, complexity, team size, regulatory requirements, and expected lifetime.

Dependency inversion, inversion of control and dependency injection

Dependency inversion states that high-level policy should not depend directly on low-level detail; both should depend on abstractions. For example, an order-processing use case should not construct a concrete PostgreSQL client. It should depend on an order repository interface. A database adapter can implement that interface.

Inversion of control describes the transfer of control over object creation or program flow from application code to a framework or container. Dependency injection is one technique for achieving this transfer. Instead of a class constructing its dependencies, dependencies are supplied through a constructor, method, or property.

Consider a conceptual TypeScript example:

```ts

interface PaymentGateway {

charge(amount: number, token: string): Promise<void>;

}

class CheckoutService {

constructor(private readonly gateway: PaymentGateway) {}

async execute(amount: number, token: string) {

await this.gateway.charge(amount, token);

}

}

```

The service is independent of Stripe, a test fake, or another provider. In NestJS, a provider token can connect `PaymentGateway` to a concrete adapter. In Spring, an interface and `@Bean` or component configuration can fulfil the same role. In Django, dependency injection is less central as a built-in framework mechanism, but dependencies can still be passed explicitly to service functions or classes.

Dependency injection improves substitution and testing, but it is not automatically beneficial. Excessive interfaces, hidden container magic, or dozens of tiny providers can make a system harder to understand. Dependencies should be explicit, meaningful, and limited to what a component actually requires.

Applying the architecture in three ecosystems

TypeScript and JavaScript with Express

Express provides routing and middleware, leaving much of the application structure to the developer. A maintainable Express project commonly separates routes, controllers, application services, repositories, configuration, and shared infrastructure. A feature-oriented arrangement might be:

```text

src/

modules/

orders/

order.routes.ts

order.controller.ts

order.service.ts

order.repository.ts

order.schema.ts

infrastructure/

database/

http/

config/

app.ts

server.ts

```

`app.ts` should assemble the Express application, middleware, routes, and error handling. `server.ts` should be responsible for starting the process and listening on a port. Separating these concerns allows tests to import the application without opening a network socket.

TypeScript improves maintainability through interfaces, discriminated unions, strict compiler settings, and explicit return types. Static types do not validate untrusted runtime input, however. Request bodies must still be validated with a runtime schema library or equivalent logic. A declared type is a compile-time promise; it does not prove that a JSON request satisfies that type.

Express middleware should have narrow responsibilities. Authentication middleware may establish user identity, validation middleware may reject malformed data, and centralized error middleware may translate known application errors into consistent responses. Controllers should avoid embedding SQL, complex pricing policies, or calls to multiple unrelated external services.

NestJS

NestJS supplies modules, controllers, injectable providers, pipes, guards, interceptors, and exception filters. Its conventions make large TypeScript applications more uniform and support dependency injection through the framework container.

A NestJS module should represent a coherent capability and explicitly declare providers and exported services. Controllers handle transport-level concerns, while providers contain application operations. Guards are appropriate for authorization or authentication decisions tied to request execution; pipes are useful for transformation and validation; interceptors can implement cross-cutting behaviour such as timing or response mapping.

NestJS can encourage useful discipline, but decorators and module metadata can conceal dependencies. Teams should document module boundaries and avoid creating a single global module that exposes every provider. A framework feature should be used because it clarifies a responsibility, not merely because it exists.

Django and Django REST Framework

Django organizes a project into settings and applications. A Django application should represent a coherent area of functionality, although the default model-view-template terminology can lead developers to place too much business logic in models or views. Django’s ORM is productive, but an ORM model is not necessarily the complete domain model.

Django REST Framework provides serializers, viewsets, routers, authentication mechanisms, permissions, throttling, and API-oriented response handling. Serializers validate and transform representations; permissions express access decisions; views coordinate the request; and domain or service functions can contain multi-step business operations.

For simple CRUD resources, a model, serializer, and viewset may be sufficient. For complex workflows, a service layer or explicit use-case functions can prevent views from becoming procedural scripts. Transactions should be applied around operations that must succeed or fail together. Query performance requires deliberate use of filtering, indexing, `select_related`, `prefetch_related`, and pagination. Framework convenience does not remove the need to understand database behaviour.

Django settings should be separated by environment without copying secrets into source control. A base configuration may define common behaviour, while development, testing, and production configuration supplies environment-specific values. Modern deployments often use environment variables or a secret-management system, with validation at startup.

Java or Kotlin with Spring Boot

Spring Boot uses convention, auto-configuration, starter dependencies, embedded servers, and externalized configuration to reduce setup effort. A common structure contains controllers, application services, repositories, domain objects, configuration, and adapters. Spring Data can implement repository operations, but an application should not allow persistence abstractions to dictate every domain decision.

Spring’s container discovers or registers beans and injects them, preferably through constructors. Constructor injection makes required dependencies visible and supports immutable fields. Profiles such as `dev`, `test`, and `prod` can activate environment-specific beans or properties, but profiles should not become a maze of mutually inconsistent application behaviour.

Kotlin adds null-safety, concise data classes, and expressive syntax, but interoperability with Java libraries requires attention to nullability annotations, proxying, and framework conventions. Whether Java or Kotlin is selected, consistent style, build reproducibility, observability, and testing matter more than language preference alone.

Spring applications should distinguish configuration properties from business data. Typed configuration objects make required settings visible and allow validation. Actuator endpoints can support health and metrics, but exposure must be secured and limited. Transaction boundaries should be explicit, particularly when a service coordinates several repository operations.

Configuration profiles and environment management

Configuration includes values that vary across environments or deployments: database URLs, credentials, feature flags, logging levels, external service endpoints, and timeouts. Configuration should not be hard-coded in business logic, nor should secrets be committed to a repository.

A sound configuration strategy has four properties. It is externalized, so deployment-specific values can change without rebuilding application logic. It is validated, so missing or malformed values cause a clear startup failure. It is typed or centrally represented, so consumers do not repeatedly parse arbitrary strings. It is observable in a safe manner, so operators can identify the active environment without revealing credentials.

Profiles provide controlled variation. Express applications may load environment-specific configuration through a configuration module; Django uses settings modules or environment-driven settings; Spring Boot uses profiles and property files. Testing requires special care: a test profile should use isolated databases, deterministic clocks, fake external providers, and safe credentials. A development profile must never accidentally point to production data.

Feature flags deserve separate treatment from deployment configuration. A database host is operational configuration; whether a new checkout flow is enabled is a product or release decision. Both may be externally controlled, but they require different ownership, audit, and retirement processes.

Package management and reproducible builds

Package management has three related concerns: declaring direct dependencies, resolving transitive dependencies, and reproducing a known dependency graph. A project should distinguish runtime dependencies from development and test dependencies. Lockfiles or equivalent dependency constraints should be committed when the tooling supports them.

Teams should update dependencies deliberately, review release notes, scan for known vulnerabilities, and remove unused packages. A vulnerable transitive package may require upgrading a direct dependency or applying a temporary resolution override. Blindly accepting every update is as risky as never updating.

npm projects use `package.json` and usually a lockfile such as `package-lock.json`. Python projects commonly use a virtual environment and a declared requirements or project metadata file, with a lock or constraints mechanism selected by the team. Maven uses a `pom.xml`, while Gradle uses build scripts and dependency locking facilities. Build tools also encode compilation, testing, packaging, and sometimes static analysis, making them part of the project’s executable architecture.

Dependency selection includes legal and organizational considerations. Licences, maintenance activity, supply-chain provenance, permissions, and data-handling behaviour should be reviewed. A package that is technically convenient may be unsuitable if it introduces unacceptable operational or licensing risk.

An illustrative case: restructuring a payment platform

Background

A fictional company, Meridian Retail, operates an online marketplace. Its initial Node.js application began as an Express prototype and grew to include product search, checkout, refunds, notifications, and administrator functions. The application used one large route file, direct database calls in handlers, environment variables read throughout the codebase, and several unpinned dependencies.

Situation and problem

Releases had become slow because a change to refunds frequently affected checkout tests. Developers could not run the full application locally without access to shared services. A production incident occurred when a development configuration pointed to a real notification provider. The company planned to add a mobile client and a second payment provider.

Analysis

The primary problem was not Express itself. The application had weak boundaries and uncontrolled dependency direction. HTTP handlers contained business rules, persistence details, and provider-specific decisions. Configuration had no central validation. Tests relied on concrete infrastructure. The team also treated a package update as an isolated developer action rather than a supply-chain decision.

Approach

The team retained a modular monolith and organized code by business capability: checkout, refunds, catalogue, and notifications. Each module defined application services and repository or gateway interfaces. Express controllers became thin adapters. Concrete database and payment implementations were placed in infrastructure modules.

A configuration module loaded and validated environment variables at startup. Test configuration supplied an in-memory or controlled database and fake payment and notification adapters. The payment use case depended on a `PaymentGateway` interface, allowing two providers to be selected through configuration without changing checkout rules.

The team enabled strict TypeScript settings, introduced runtime request validation, committed the lockfile, and added automated dependency scanning. It also separated application assembly from server startup so integration tests could create the app without binding a port. Architectural decisions and module ownership were recorded in a short decision log.

Outcome

The restructured system allowed checkout tests to run without contacting a payment provider. Adding a second provider required a new adapter and configuration change rather than modifications throughout the checkout logic. The incident risk associated with accidental production notifications was reduced through environment validation and safe test defaults. The team still accepted that the modular monolith would eventually require extraction only if independent scaling, deployment, or ownership justified it.

Learning points

The case illustrates that maintainability is achieved through boundaries, not through the number of folders or the prestige of a framework. Dependency inversion made provider substitution possible; profiles reduced configuration risk; package discipline improved reproducibility; and a modular monolith preserved operational simplicity while enabling internal separation.

Challenges, failure modes and professional judgement

A common mistake is equating a framework’s default folder structure with architecture. Folders are useful only when they communicate responsibility and dependency direction. Another mistake is creating generic layers that every feature must traverse, even when a simple operation needs only one clear component. Excessive abstraction can be as harmful as no abstraction.

Circular dependencies indicate unclear ownership. They may appear as circular module imports in Node.js, circular application dependencies in Spring, or tightly coupled Django applications. The remedy is usually to move a policy to the component that owns it, introduce a narrower interface, or reorganize the feature boundary—not merely to suppress the error.

Framework leakage is another risk. If business rules depend directly on HTTP request objects, ORM query sets, or framework exceptions, reuse and testing become difficult. Some leakage is practical and acceptable, particularly in small systems, but it should be a conscious trade-off.

Other recurring failures include placing secrets in repositories, using mutable global state, catching and discarding exceptions, returning inconsistent error formats, omitting timeouts for external calls, ignoring transaction boundaries, and allowing unbounded list endpoints. Architecture must include operational qualities such as logging, metrics, tracing, graceful shutdown, and health checks.

Governance, security and ethical responsibility

Architecture determines how an organization handles personal data, access control, auditability, and failure. Authentication establishes identity; authorization determines permitted actions. These concerns should not be hidden inside ad hoc controller conditions. Sensitive operations should have explicit policies and, where appropriate, auditable records.

Configuration and dependency governance are security controls. Secrets should be managed through protected environment facilities or secret managers. Logs should avoid passwords, tokens, and unnecessary personal data. APIs should validate input, enforce authorization at the correct resource boundary, apply rate controls where necessary, and expose only intended error details.

Data protection also affects modular design. A notification module should receive only the information it needs, rather than unrestricted access to customer records. Retention, deletion, consent, and data residency requirements may require architectural support. Professional responsibility includes considering misuse and unequal impact, especially when backend services support automated decisions.

Emerging directions

Modern backend teams increasingly use typed contracts, generated clients, containerized development environments, and automated build pipelines. OpenAPI specifications, schema validation, and contract testing can reduce disagreement between frontend and backend teams. Observability is becoming a design concern rather than a production afterthought.

Serverless platforms, event-driven systems, and managed services alter deployment and operational boundaries but do not remove the need for modularity, configuration control, and dependency inversion. Artificial intelligence can assist with code generation, test creation, dependency analysis, and documentation. Generated code must still undergo review for security, licensing, correctness, data leakage, and architectural fit. AI tools should not be treated as substitutes for ownership of design decisions.

Best-practice guidance

A professional team should begin with business capabilities and quality requirements, then select framework features that support them. It should keep controllers thin, make dependencies explicit, centralize and validate configuration, isolate external systems behind adapters where volatility justifies it, and use constructor injection or explicit parameter passing for required dependencies. It should maintain reproducible builds, review dependency changes, test failure paths, and document significant architectural decisions.

The best architecture is proportionate architecture. A small service may need only clear modules, validation, centralized errors, and disciplined configuration. A high-risk platform may require ports and adapters, formal threat modelling, contract tests, audit trails, and stronger governance. The professional skill lies in recognizing the difference.

Chapter Summary

Backend maintainability depends on the relationship between language, runtime, framework, dependencies, and architecture. Express offers flexibility, NestJS offers structured TypeScript conventions, Django combines batteries-included development with extensibility, Django REST Framework supports API construction, and Spring Boot provides convention-driven configuration over the Spring ecosystem. None guarantees good design.

Modularity, cohesion, low coupling, separation of concerns, dependency inversion, inversion of control, and dependency injection provide the conceptual foundation for sustainable systems. Layered, feature-oriented, hexagonal, and clean architectures are useful perspectives rather than rigid recipes. Configuration profiles should externalize and validate environment-specific values, while package-management practices should support reproducibility, security, and legal compliance. A modular monolith is often an effective intermediate choice, and abstraction should be proportionate to complexity and risk.

Key Terms

Programming language: A formal system for expressing computation and program behaviour.

Runtime: The execution environment that runs application code.

Framework: A structured software platform that controls application flow and provides extension points.

Library: Reusable code explicitly called by an application.

Package manager: Tooling that declares, resolves, installs, and updates software dependencies.

Modularity: Organization of software into components with defined responsibilities and interfaces.

Cohesion: The degree to which responsibilities within a component belong together.

Coupling: The degree of dependency between components.

Separation of concerns: Assignment of different responsibilities to distinct components.

Inversion of control: Transfer of control over application flow or object creation to a framework or container.

Dependency injection: Supplying a component’s dependencies rather than constructing them internally.

Dependency inversion: Design principle in which high-level policy depends on abstractions rather than concrete details.

Layered architecture: Organization of software into technical responsibility layers.

Hexagonal architecture: Architecture that isolates a core through ports and connects it to external adapters.

Adapter: A component that translates an external technology or interface into an application boundary.

Profile: A named configuration variation for a particular environment or operating mode.

Lockfile: A file recording resolved dependency versions for reproducible installation.

Modular monolith: A single deployable application containing internally separated modules.

Configuration drift: Unintended divergence among environments or configuration sources.

Observability: The ability to understand system behaviour through logs, metrics, traces, and related signals.

Review Questions

Short Answer Questions

1. Distinguish a framework from a library and give one backend example of each.

2. What is the relationship between inversion of control and dependency injection?

3. Why should runtime validation be used in a TypeScript API?

4. State two purposes of environment-specific configuration profiles.

5. What is a modular monolith?

Long Answer Questions

1. Explain cohesion, coupling, and separation of concerns and show how they influence backend project structure.

2. Compare Express, NestJS, Django REST Framework, and Spring Boot as architectural environments.

3. Describe how hexagonal architecture can isolate a payment provider from application business rules.

4. Discuss package management as a concern involving reproducibility, security, and licensing.

5. Explain the benefits and risks of dependency injection in a large backend system.

Analytical / Application Questions

1. A Django view performs validation, pricing, database writes, and email delivery. Analyse the design and propose a more maintainable structure.

2. A Spring Boot service works in development but connects to production data during testing. Identify the architectural and governance failures and recommend controls.

3. An Express application must support two payment providers and a mobile API without becoming a microservice system. Design an appropriate modular architecture and justify your choices.

22. Multiple Choice Questions with Answers

Question 1

Which design best demonstrates dependency inversion in a backend application?

A. A controller directly constructs a PostgreSQL client for every request.

B. A use-case service depends on a repository interface implemented by a database adapter.

C. A model imports every service that may use it.

D. A configuration file contains SQL statements for each controller.

Correct Answer: B. A use-case service depends on a repository interface implemented by a database adapter.

Question 2

Why is separating app assembly from server startup useful in an Express project?

A. It eliminates the need for request validation.

B. It allows tests to import the application without opening a network port.

C. It prevents all runtime errors.

D. It automatically creates database migrations.

Correct Answer: B. It allows tests to import the application without opening a network port.

Question 3

Which statement about TypeScript types and HTTP input is correct?

A. TypeScript types automatically validate JSON received at runtime.

B. Runtime validation is unnecessary when strict mode is enabled.

C. TypeScript improves compile-time guarantees, but untrusted input still requires runtime validation.

D. Types can replace authentication and authorization.

Correct Answer: C. TypeScript improves compile-time guarantees, but untrusted input still requires runtime validation.

Question 4

In a hexagonal architecture, what is the role of an adapter?

A. It defines all business policies in the domain core.

B. It translates an external technology or interface into an application boundary.

C. It replaces the need for automated tests.

D. It stores all environment variables.

Correct Answer: B. It translates an external technology or interface into an application boundary.

Question 5

Which configuration practice presents the greatest security risk?

A. Validating required settings during startup.

B. Using separate test and production profiles.

C. Committing database credentials to source control.

D. Loading deployment-specific values externally.

Correct Answer: C. Committing database credentials to source control.

Question 6

A Django REST Framework serializer is primarily responsible for which concern?

A. Operating the web server process.

B. Validating and transforming data representations.

C. Managing JVM memory.

D. Selecting npm package versions.

Correct Answer: B. Validating and transforming data representations.

Question 7

Why is constructor injection generally preferred for required Spring dependencies?

A. It hides dependencies from readers of the class.

B. It makes required dependencies explicit and supports immutable fields.

C. It disables the Spring container.

D. It guarantees that database queries are optimized.

Correct Answer: B. It makes required dependencies explicit and supports immutable fields.

Question 8

Which situation most strongly justifies a modular monolith rather than immediate microservices?

A. The application has no identifiable business capabilities.

B. The team needs internal boundaries but does not yet require independent deployment or scaling.

C. Every function must run in a separate process.

D. The organization wants to avoid all automated testing.

Correct Answer: B. The team needs internal boundaries but does not yet require independent deployment or scaling.

Question 9

What is the main purpose of a dependency lockfile?

A. To record resolved dependency versions for reproducible installation.

B. To encrypt application secrets.

C. To define HTTP authorization rules.

D. To replace source-code version control.

Correct Answer: A. To record resolved dependency versions for reproducible installation.

Question 10

A checkout service must support multiple payment providers. Which design is most maintainable?

A. Place provider-specific conditionals in every controller.

B. Make the use case depend on a payment-gateway abstraction and provide provider-specific adapters.

C. Allow each provider to modify the order database directly.

D. Duplicate the complete checkout workflow for each provider.

Correct Answer: B. Make the use case depend on a payment-gateway abstraction and provide provider-specific adapters.

Advanced Backend Development (Node.js, Django, Spring Boot)
Chapter 2 of 15