Foundation of a Software Application
Why is This Important?
We don't always get the privilege to build an application from scratch. But when we do, creating a solid foundation is one of the most important tasks to ensure the success of the project. In this section, we go into details
Decisions, Decisions
As a Sr. Software Developer, we have to constantly make decisions, from little details to very big transcendental choices that will have ramifications for years to come.
In the introduction of the book, we discussed how some of the decisions we have to make can have transcendental implications for an organization. With this in mind, we can start thinking about the specific decisions we have to make to get a software project started from scratch. In the context of this book, this software project can take different shapes. It can be a back-end service that our organization will host, a software product that our customers will run, a shared component meant to be consumed by other projects, etc.
These basic decisions include:
- Programming language
- Language version to target
- Frameworks to adopt
- Libraries to use
- Where to store the code
- Number of code repositories and their structure
- The structure of the code
A lot of these are already defined in large organizations, so we always need to take that context into account. Nonetheless, we're going to go through the different decisions in detail in the following sections.
Choosing the Language and Language Version
When faced with a new project, many developers want to dive right in and start writing code.
But before we print that first Hello World!
, we need to know which programming language we're going to use.
Many times this will be obvious, but we're still going to explore the process through which we arrive at a decision.
To choose the language and the specific language version, it's important to understand the constraints under which we're operating.
- What language or languages does our team know?
- What languages does our organization support?
- If our code is going to be shipped to customers, what languages do our customers favor?
- Do we have required language-specific dependencies?
Languages Within the Team
Most teams are assembled around a particular skill set, with language familiarity being one of the primary skills considered. Some team members will be motivated to try and learn new languages, while other team members would rather specialize in one primary language and would rather not fork to learn a new language.
Understanding the skillset (and willingness to learn new ones) of our team is the first step to making the decision regarding which language to use.
With that said, some shifts are relatively minor and might be more feasible, in particular when we're shifting to a related language.
The smallest shift is when we upgrade the major version of our language. For example, upgrading from Python 2 to Python 3 is a small but significant change. The syntax is similar, but not exactly equal, and many of the third-party libraries are not compatible from 2 to 3.
A bigger shift is when we move to a related language in the same family. For example, a Java team could make a jump Kotlin with relatively little trouble. Kotlin is heavily influenced by Java, runs on the same JVM (Java Virtual Machine), can interoperate with Java, has the same threading model, uses the same dependency management mechanism, etc. Both languages share the same programming paradigm and ecosystem, and their syntax is not that dissimilar.
Similarly, moving from JavaScript to TypeScript is a much smaller shift since they share most of the syntax and the same programming paradigm and ecosystem.
Bigger jumps, for example from Java to JavaScript or TypeScript, need to be very well supported. These changes must be very well supported, since they require not only learning a new syntax, but also a new ecosystem, and potentially a new programming paradigm.
Languages Within the Organization
The popularization of container-based workflows (Docker containers in particular) has opened up the gates for organizations to be able to support a lot more different languages. Suddenly we can support more runtimes, more heterogeneous build pipelines, etc. However, just because one can build it and run it, doesn't mean that the organization is fully set up to support a new language or even a different version of a language.
We need to ensure that the language we choose is supported by the different infrastructure that plays a part in the wider SDLC:
- Artifact registries
- Security scanners
- Monitoring tools
In the same sense, it might be the organization itself forcing the change, because they're dropping support for our language of choice (or a particular version).
Languages and External Support
As we're choosing our language we must also understand if there are external constraints that we need to satisfy.
Some of these constraints might be hard. For example, we might have a hard dependency on a close source library that is only supported in a particular language, or maybe even a particular version of a language. In such cases, we must decide within those constraints.
In other cases, constraints might be softer. If our software is going to be shipped to external users packaged as an application, there might be pushback to change the runtime. For example, there might be pushback to require our users to change their Java Runtime Environment. In these cases, there might be valid reasons to push our users to make those changes, even against any pushback. A runtime reaching end-of-life is a very obvious one, but providing new features or better performance might make forcing the change more appealing.
Language Features
Once we have taken into account the constraints in which we can select the language based on the area that excites us developers: features!. What language provides the features that will make it easier to write the best software?
Do we want an object-oriented language like Java? or a more multi-purpose language that supports both an object and procedural approach like Python?
Does a particular runtime provide a better performance for our particular use case? What threading model works best for us? In Chapter 11 we take a look at the difference in threading models for the JVM, NodeJS, and Python.
Which ecosystem has the libraries and frameworks that will help us the most? For example, in the area of Data Science, Python has a very rich ecosystem.
And within one particular language, do we want to target a particular minimum version to get any new features that will make our lives easier?
- What language are we going to use?
- What version of the language are we going to target?
Choosing the Framework
Once we decide on a particular language to target, we move up to the next level of the foundation. In modern software development, we rarely build things from scratch, but rather leverage frameworks that provide a lot of core functionality so that we can focus on the functionality that will differentiate our software.
In choosing the framework we have to consider what services it provides and contrast that with the services that our application will need.
There are many different frameworks and many types of frameworks that provide different functionality. For example:
Many different types of frameworks:
- Dependency Injection
- Model View Controller (MVC)
- RESTful and Web Services
- Persistence
- Messaging and Enterprise Integration Patterns (EIP)
- Batching and Scheduling
Some frameworks will cover multiple areas, but many times we still have to mix and match. To select a framework or frameworks we need to determine the functionality that our application will use, and then select the
This must be done under the same constraints that we used to select our language:
- Knowledge and familiarity within our team
- Support within the company
- Compatibility with any external components with which we have a hard dependency
- If we're shipping our code to a customer, support among them
In the following sections, we're going to introduce the three frameworks that we will focus on.
Spring Boot
Spring Boot is a Java framework that packages a large number of Spring components, and provides "opinionated" defaults that minimize the configuration required.
Spring Boot is a modular framework and includes components such as:
- Spring Dependency Injection
- Spring MVC
- Spring Data (Persistence)
Many other modules can be added to provide more functionality.
Spring Boot is often used for RESTful applications, but can also be used to build other kinds of applications, such as event-driven or batch applications.
Express
Express (sometimes referred to as Express.js) is a back-end web application framework for building RESTful APIs with Node.js. As a framework oriented around web applications and web services, Express provides a robust routing mechanism, and many helpers for HTTP operations (redirection, caching, etc.)
Express doesn't provide dedicated Dependency Injection, but it can be achieved using core Javascript functionality.
Flask
Flask is a web framework for Python. It is geared toward web applications and RESTful APIs.
As a "micro-framework", Flask focuses on a very specific feature set. To fill any gaps, flask supports extensions that can add application features. Extensions exist for object-relational mappers, form validation, upload handling, and various open authentication technologies. For example, Flask benefits from being integrated with a dependency injection framework (such as the aptly named Dependency Injector).
- What features do we need from our framework or frameworks?
- What framework or frameworks are we going to use?
Other Frameworks
In this section, we introduced some of the frameworks that we're going to focus on throughout the book. We have selected these due to their popularity, but there are many other high-quality options out there to consider.
Libraries
A framework provides a lot of functionality, but there is normally a need to fill in gaps with extra libraries. Normally these extra libraries are either related to the core functional requirements or help with cross-cutting concerns.
For the core functionality, the required libraries will be based on the subject expertise of the functionality itself.
For the libraries that will provide more generic functionality, we cover cross-cutting concerns in future sections.
Version Control System
Keeping the source code secure is paramount to having a healthy development experience. Code must be stored in a Version Control System (VCS) or a Source Code Management (SCM) system. Nowadays most teams choose Git for their version control system, and it is widely considered the industry-wide de-facto standard. However, there are other modern options, both open source and proprietary, that bring some differentiation that might be important to your team. Also, depending on your organization you might be forced to use a legacy Version Control System, although this is becoming less and less frequent.
From here on out, the book will assume Git as the Version Control System.
Repository Structure
When we're getting ready to store our source code in our Version Control System, we need to make a few decisions related to how we're going to structure the code.
We might choose to have a single repository for our application, regardless of how many components (binaries, libraries) it contains. We can separate the different components in different directories while having a single place for all of the code for our application.
In some cases, it might be necessary to separate some components or modules into different repositories. This might be required in a few cases:
- Part of the code will be shared with external parties. For example, a client library will be released as open source, while the server-side code remains proprietary
- It is expected that in the future, different components will be managed by other teams and we want to completely delegate to that team when the time comes.
There's another setup that deserves special mention, and it's the monorepo concept. In this setup, the code for many projects is stored in the same repository. This setup contrasts with a polyrepo, where each application, or even each component in an application has its own repository. Monorepos have a lot of advantages, especially in large organizations, but the design and implementation of such a system is very complex, requires an organization-wide commitment, and can take months if not years. Therefore choosing a monorepo is not a decision that can be taken lightly or done in isolation.
Pros | Cons | |
---|---|---|
One repo per application |
|
|
One repo per module |
|
|
Monorepo |
|
|
Git Branching Strategies
One mechanism that must be defined early in the development process is the Git branching strategy. This defines how and why development branches will be created, and how they will be merged back into the long-lived branches from which releases are created. We're going to focus on three different strategies that are well-defined:
GitFlow
GitFlow is a Git workflow that involves the use of feature branches and multiple primary branches.
The main
or master
branch holds the most recently released version of the code.
A develop
branch is created off the main
or master
branch. Feature branches are created from develop
. Once development is done in feature branches, the changes are merged back into develop
.
Once develop
has acquired enough features for a release (or a predetermined release date is approaching), a release
branch is forked off of develop
. Testing is done on this release
branch. If any bugfixes are needed while testing the release
branch a releasefix
branch is created. Fixes must then be merged to the release
branch as well as the develop
branch. Once the code from the release
is released, the code is merged into main
or master
. During the time the release is being tested, new feature releases can still occur in the develop
branch.
If any fixes are needed after a release a hotfix
branch is created. Fixes from the hotfix
branch must be merged back into develop
and main
/master
.
GitFlow is a heavy-weight workflow and works best for projects that are doing Semantic Versioning or Calendar Versioning.
GitHub Flow
GitHub Flow is a lightweight, branch-based workflow.
In GitHub Flow, feature branches are created from the main
or master
branch.
Development is done in the feature branches.
If the development of the feature branch takes a long time,
it should be refreshed from main
or master
regularly.
Once development is complete, a pull request is created to merge the code back into main
or master
.
The code that is merged into main
or master
should be ready to deploy. No incomplete code should be merged.
GitHub Flow works best for projects that are doing Continous Delivery (CD).
Trunk-Based
Trunk-based development is a workflow where each developer divides their work into small batches.
The developer merges their work back into the trunk
branch regularly, once a day or potentially more often.
The developers must also merge changes from trunk
back into their branches often, to limit the possibility of conflicts.
Trunk-based development is very similar to GitHub Flow, the main difference being that GitHub Flow has longer-lived branches with larger commits.
Trunk-based development forces a reconsideration of the scope of the tickets, to break up the work into smaller chunks that can be integrated regularly back into the trunk
.
- What Version Control System will we use?
- How many repositories will our application use?
- Which branching strategy will the team use?
Project Generators
Most frameworks provide tools that make it easy to create the basic file structure. This saves the effort of having to find the right formats for the files and creating them by hand, which can be an error-prone process.
This functionality might be integrated as part of the build tools for your particular ecosystem.
For example Maven's mvn archetype:generate
, Gradle's gradle init
, and NPM's npm init
will generate the file structure for a new project.
These tools take basic input such as the component's name, id, and version and generate the corresponding files.
Some stand-alone CLI tools provide this functionality and tend to have more features. Some examples are express-generatorhttps://expressjs.com/en/starter/generator.html, jhipster, and Spring Boot CLI.
Another option is to use a website that will build the basic file structure for your project from a very user-friendly webpage. For example Spring Initializr generates spring boot projects, and allows you to select extra modules.
Finally, your organization might provide its own generator, that will set up the basic file structure for your project, while also defining organization-specific data such as cost codes.
- What tool or template are we going to use to create the basic file structure of our component(s)?
Project Structure
The basic structure we choose for our code will depend on how our code will be used.
If we're creating a request processing application we can consider how the requests or operations will flow through the code, how the application is exposed, and what external systems or data sources we will rely on. A lot of these questions make us step into "architect" territory, but at this level, there's a lot of overlap.
The structure of our code will vary significantly if we're building a service compared to if we're building a library that will be used by other teams. How we structure the code might also vary if we're keeping our code internal, or if it's going to be made available to outside parties.
In terms of the project structure, the layout of the files should lend itself to easily navigating the source code. In this respect, the layout normally follows the architecture, with different packages for different architectural layers or cross-cutting concerns.
Given the large number of variations and caveats, we're going to focus on very basic for general purpose architecture that we detail in the next section.
Basic Backend Architecture
Code can always be refactored, but ensuring we have a sound architecture from the start will simplify development and result in higher quality. When we talk about higher-quality code, we mean not only code with fewer bugs but also code that is easier to maintain.
Applications will vary greatly, but for most backend applications, we can rely on a basic architecture that will cover most cases:
This architecture separates the code according to the way requests flow. The flow starts in the "Controllers & Endpoints" when requests are received, moving down the stack to "Business Logic" where business rules and logic are executed, and finally delegating to "Data Access Objects" that abstract and provide access to external systems.
Other classes would fall into the following categories:
- Domain Model
- Utility Classes and Functions
- Cross-cutting Concerns
In such an architecture, the file layout will look like this:
src
├───config
├───controllers
│ └───handlers
├───dao
│ └───interceptors
├───model
│ └───external
├───services
│ └───interceptors
└───utils
This structure is based on the function of each component:
config
: Basic configuration for the application.controllers
: Controllers and endpoints.controllers/handlers
: Cross-cutting concerns that are applied to controllers or endpoints, such as authentication or error handling.dao
: Data access objects that we use to communicate with downstream services such as databases.dao/interceptors
: Cross-cutting concerns that are applied to the data access objects, such as caching and retry logic.model
: Domain object models. The classes that represent our business entities.model/external
: Represents the entities that are used by users to call our service. These are part of the contract we provide, so any changes must be carefully considered.services
: The business logic layer. Services encapsulate any business rules, as well as orchestrating the calls to downstream services.services/interceptors
: Cross-cutting concerns that are applied to the services, such as caching or logging.utils
: Utilities that are accessed statically from other multiple components.
This file structure is only provided as an example. Different names and layouts can be used as needed, as long as they allow the team to navigate the source with ease. There will normally be a parallel folder with the same structure for the unit tests.
Controllers and Endpoints
Controllers and Endpoints comprise the topmost layer, These are the components that initiate operations for our backend service. These components are normally one of the following:
- The REST or API endpoint listening for HTTP traffic.
- The
Controller
when talking about MVC (Model View Controller) applications. - The Listener for event-driven applications.
The exact functionality of the component depends on how the type of application, but its main purpose is to handle the boundary between our application and the user or system that is invoking our service.
For example, for HTTP-based services (web services, MVC applications, etc), in this layer we would:
- Define the URL that the endpoint will respond to
- Receive the request from the caller
- Do basic formatting or translation
- Call the corresponding Business Logic
- Send the response back to the caller
For event-driven applications, the components in the layer would:
- Define the source of the events
- Poll for new events (or receive the events in a pull setup)
- Manage the prefetch queue
- Do basic formatting or translation
- Call the corresponding Business Logic
- Send the acknowledgment (or negative acknowledgment) back to the event source
In both the HTTP and the event-driven services, most of this functionality is already provided by the framework, and we just need to configure it.
There is one more case that belongs on this layer, and it relates to scheduled jobs. For services that operate on a schedule, the topmost layer is the scheduler. The scheduler handles the timing of the invocation as well as the target method within the Business Logic layer that will invoked. In such cases, we can also leverage a framework that will provide a scheduler functionality.
Business Logic
The middle layer is where our "Business Logic" lives.
This includes any complicated algorithm, business rules, etc.
It is in this layer that the orchestration of any downstream calls occurs.
The components in this layer are normally called Services
.
This layer should be abstracted away from the nuanced handling of I/O.
This separation makes it easier for us to mock upstream and downstream components and easily create unit tests.
It is in this layer where the expertise of business analysts and subject matter experts is most important, and their input is critical.
Involving them in the design process, and working with them to validate the test cases. We go into detail about specific techniques in Chapter 5.
Data Access Objects
The bottom-most layer provides a thin layer to access downstream services. Some of these downstream services can be:
- Databases: If our service needs to read and write data from a database.
- Web Services: Our service might need to talk to other services over a mechanism such as REST or gRPC.
- Publishing Messages: Sometimes our application might need to send a message, through a message broker or even an email through SMTP (Simple Mail Transfer Protocol).
Using a Data Access Object helps to encapsulate the complexity of dealing with the external system, such as managing connections, handling exceptions and retries, and marshaling and unmarshalling messages.
The objective of separating the data access code into a separate is to make it easier to test the code (in particular the business logic code). This level of abstraction also makes it easier to provide a different implementation at runtime, for example, to talk to an in-memory database rather than to an external database.
In a few cases, there might not be a need for a Data Access Object layer. For applications that rely only on the algorithms present in the Business Logic layer and don't need to communicate to downstream services, the Data Access layer is irrelevant.
Domain Model
The Domain Model is the set of objects that represent entities we use in our systems. These objects model the attributes of each object and the relationships between different objects. The Domain Model defines the entities our service is going to consume, manipulate, persist, and produce.
In some cases, it's important to separate the external model from the internal model. The external model is part of the contract used by callers. Any changes to the external model should be done with a lot of care, to prevent breaking external callers. More details on how to roll out changes to the external model can be found in Chapter 14 The internal model is only used by our application or service and can be changed as needed to support new features or optimizations.
In traditional Domain-Driven Design (DDD), the Domain Model incorporates both behavior and data. However, nowadays most behavior is extracted to the business layer, leaving the Domain Model as a very simple data structure. Nonetheless, the correct modeling of the domain objects is critical to producing maintainable code. The work of designing the Domain Model should be done leveraging the expertise of the business analysts and subject matter experts.
The Domain Model can be represented by an Entity Relationship Diagram (ERD).
Utility Classes and Functions
In this context "utility" is a catch-all for all components (classes, functions, or methods) that are used to perform common routines in all layers of our application.
These utility classes and functions should be statically accessible. Because of their wide use, it does not make sense to add them to a superclass. Likewise, utility classes are not meant to be subclassed and will be marked as such in languages that support this idiom (for example final
in Java).
Cross-Cutting Concerns
Cross-cutting concerns are parts of a program that affect many other parts of the system. Extracting cross-cutting concerns has many advantages when writing or maintaining a piece of software:
- Reusability: Allows the code to be used in different parts of the service.
- Stability and reliability: Extracting the cross-cutting concern makes it easier to test, increasing the stability and reliability of our service.
- Easier extensibility: Using a framework that supports cross-cutting concerns makes it easier to extend our software in the future.
- Single responsibility pattern: It helps ensure that our different components have a single responsibility.
- SOLID and DRY: It makes it easier to follow best practices such as SOLID and DRY(Don't Repeat Yourself).
To implement the cross-cutting concerns we want to leverage our framework as much as possible. Many frameworks provide hooks that we can use to save time. However, we also want to understand how the framework implements the hooks, to be able to debug and optimize these components. We explore in more detail some of the techniques used by common frameworks in Chapter 13.
There are some disadvantages to abstracting away cross-cutting concerns, especially when relying on "convention over configuration". Some developers might have a harder time following the logic if they don't understand the framework. Also "convention over configuration" often conflicts with another software design principle: "explicit is better than implicit". As Senior Software Developers, we must balance the ease of maintaining and ease of testing with the technology and functionality we're bringing into the code base.
The design of how the cross-cutting concerns will be implemented is a vital part of the architecture of the application. These are important decisions, but one of the advantages of decoupling this functionality into separate components, is that it makes it easier to fix any shortcomings.
How cross-cutting concerns are supported varies from framework to framework. Here are some common mechanisms:
- Aspect Oriented Programming
- Filters and Middleware
- Decorators and Annotations
These mechanisms are explained in detail in Chapter 13.
In the next sections, we'll introduce some core cross-cutting concerns. Many decisions should be made for each one of these cross-cutting concerns, but those decisions are formally listed in the respective in-depth chapters, rather than the introductions from this chapter.
Logging
Logging information about the application's behavior and events can be helpful for debugging, monitoring, and analysis. Logging is a cross-cutting concern because you want to abstract away and centralize the details of how the logging is done. These details include:
- Which library should we choose?
- Where should we write to? Most libraries allow the configuration of log sinks. Depending on how our application is configured, we might want to write or append to standard output to be picked up by an external logging agent. In other cases, we'll write directly to our logging service.
- What format should we use? Most logging libraries use plain text by default, and we can configure that plain text format with a custom pattern. Depending on which logging service we're using, we might choose a structured logging format such as JSON to be able to emit extra metadata.
The actual logging of the messages is normally left up to individual methods, but an application-wide vision is needed. For example, a Senior Developer or Architect should decide if context-aware logging is going to be used, and if so what information should be recorded as part of the context. Context-aware logging is a mechanism to enrich log messages with data that might be unavailable in the scope in which the log statement executes. This is also sometimes referred to as Mapped Debugging Context. We discuss this technique in more detail in Chapter 19.
Security
Ensuring the confidentiality, integrity, and availability of data is a critical concern for many applications. This can involve implementing authentication, authorization, and encryption mechanisms.
As Senior Developers we have to decide what mechanisms are necessary, and where they should be implemented.
For example, one of the decisions that is commonly relevant for a backend service is how will users authenticate to use our application.
There are many options to consider:
- Usernames and Passwords
- Tokens (for example OAuth or JWT)
- Certificate-based authentication
- OpenId or SAML
Due to the risks involved, it's better to use an existing framework for security rather than writing our own. Many frameworks provide out-of-the-box solutions or provide hooks to support a modular approach.
For example in the Java/Spring ecosystem Spring Security provides a very feature-rich solution.
Neither Flask nor Express provides an out-of-the-box security solution but rather provides hooks into which third-party solutions can be adapted. These third-party solutions are normally applied as middleware, functions that have access to the request and response objects, and the next middleware function in the application’s request processing chain.
Where should security be implemented?
Security can be applied at different layers. However it is mostly applied at the Controller and Endpoints layers, and in the Business Logic layer less often. In general the closer to the user the better. In some cases, the authentication and authorization is offloaded from the application, and handled by an API Gateway or a Service Mesh.
Security is a very broad topic, and there are many specialized sources on the topic, however, we do talk more in detail in Chapter 9.
Caching
Caching can help us achieve two main goals, improve performance and improve reliability. Caching can improve the performance of an application by reducing the number of trips to the database or other back-end systems. Caching can also improve the reliability of an application by keeping a copy of data that can be used when downstream services are unavailable. However, caching comes with a significant amount of extra complexity that must be considered.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
Where should caching happen in a back-end application?
Caching can be implemented at every layer of the application, depending on what we're trying to achieve. However, it's best to not implement caching directly on the Data Access Objects, to maintain a single responsibility.
Caches can be shared across multiple instances or even multiple applications. Caches can also be provisioned on a per-instance basis. All of these parameters determine the effects caching will have on reliability, scalability, and manageability. Caching also introduces the need to consider extra functionality, for example, to handle the invalidation of entries that are no longer valid or stale.
Caching is a complicated topic with many implications, and we just scratched the surface here. We talk more in detail in Chapter 11.
Error Handling
Error handling is often overlooked as a cross-cutting concern, but a good setup from the beginning will make the application more maintainable and provide a better user experience in the worst moments. First and foremost, error handling is about trying to recover from the error if at all possible. How we do this depends greatly on our application and the type of error.
However, error handling is also about handling cases when we can't recover from the error. This entails returning useful information to the user while ensuring we don't leak private or sensitive information. Approaching error handling as a cross-cutting concern can help us simplify our code while providing a homogeneous experience for our users, by allowing us to handle errors regardless of where they occur in the application.
As part of the recurring theme, we should leverage our framework to simplify the work of handling errors. Our framework can help us map exceptions to HTTP return codes. The framework can help us select the proper view template to render user-readable error messages. Finally, most frameworks will provide default error pages for any exception not handled anywhere else.
For example, in a Spring-based application we can use an HandlerExceptionResolver
and its subclasses ExceptionHandlerExceptionResolver
,
DefaultHandlerExceptionResolver
,
and ResponseStatusExceptionResolver
.
Express provides a simple yet flexible error handling mechanism, with a default error handler and hook to write custom error handlers.
By default, Flask handles errors and returns the appropriate HTTP error code. Flask also provides a mechanism to register custom error handlers.
If we refer to the core architecture we proposed, where should error handling be done?
Normally we want to handle unrecoverable errors as close to the user as possible. We do this have bubbling up exceptions up the stack until they are caught by an Exception Handler and properly dealt with. If we're talking about recoverable errors, we need to handle them in the component that knows how to recover from them, for example by retrying the transaction or using a circuit breaker pattern to fall back to a secondary system.
More detail on error handling as a cross-cutting concern can be found in (Chapter 12)[./chapter_12.md#error-handling].
Transactions
Transactions ensure that changes to data are either fully committed or fully rolled back in the event of an error, which helps maintain data consistency. Given the rise of NoSQL databases, transactions are not as widely used, but they're still very relevant when working with a traditional RDBMS (relational database management system).
Once we have determined the data sources that we will be interacting with, we have to determine if they support transactions. For data sources that don't support transactions, we have to think of other ways to reconcile failed updates.
Transactions can encompass a single data source or multiple data sources. If we have transactions across multiple data sources, we have to determine if they support a global transaction mechanism such as XA.
If we're going to use transactions, we need to select a transaction manager and integrate it into our application. If we're using a transaction manager, we must also decide how we're going to control the transactions. Transactions are either controlled programmatically or declaratively. Programmatic transactions are explicitly stated and committed. Declarative transactions are delineated by the transaction manager based on our desired behavior as indicated by special annotations (such as Transactional in Java).
Where should transaction control happen?
Transaction control normally happens in the Business Logic layer, or the gap between the Business Logic layer and the Data Access Objects layer. It is generally a bad idea to control transactions from the Controller and Endpoints layer since it would require the caller to know something about the internal workings of the lower layers, breaking the abstraction (also known as a leaky abstraction).
More details on transactions can be found in (Chapter 16)[./chapter_16.md#transactions].
Internationalization or Localization
Internationalization (sometimes referred to as "i18n") and Localization (sometimes referred to as "l10n") relate to applications that need to be used by people in different countries to be designed to handle different languages, time zones, currencies, and other cultural differences. These are very important concepts within software development but are most relevant for front-end development. Given the focus of the book on back-end development, the scope is much more limited and mostly relevant for systems that use "Service Side Scripting". In the cases where we use Server Side Scripting, the framework that we use can help us select the right template and format dates and numbers appropriately.
The concepts of internationalization or localization can also affect some of the data we work with, such as the currency that use. If our application is purely an API-based service then dates are normally returned in a standard format, and formatted by the client.
If using Spring MVC as part of your Spring application, you can use the Locale support to automatically resolve messages by using the client’s locale.
Neither Flask nor Express provide locale resolution support out of the box but can be provided by a third-party add-on.
Tools Referenced
- Spring Boot Open-source framework built around the Spring ecosystem. Mostly used to create web applications or web services, but can be used for other types of applications.
- Express Back-end web application framework for building RESTful APIs with Node.js.
- Flask A micro web framework written in Python.