Skip to content

Software Architecture

Jeremy Ho edited this page Jun 24, 2025 · 6 revisions

Software Architecture

This page outlines the general architecture and design principles of PEACH. It is mainly intended for a technical audience and for people who want to have a better understanding of how the system works.

Table of Contents

Database Schema

The PostgreSQL database is written and handled via managed, code-first migrations and additive seeding. We generally store tables containing normalized PIES data and how they relate to each other. As PEACH is a back-end microservice, lines of business can leverage PEACH without being tied to a specific framework or language. The following link describes the current working database schema of the PEACH database:

Database design focuses on simplicity and succinctness, storing the essentials to represent PIES messages. We enforce foreign key integrity by invoking onUpdate and onDelete cascades in Postgres. This ensures that we do not have dangling references when entries are removed from the system. The relationships between system records are mapped as a many-to-many relationship using the record linkage join table.

Auditing

We use a generic audit schema table to track any update and delete operations performed on the database. This table is only modified by the database via table triggers and is not normally accessible by the PEACH application itself. This should meet most general security, tracking, and auditing requirements.

Code Design

PEACH is a relatively small and compact microservice with a very focused approach to handling and managing system records. However, not all design choices are self-evident just from inspecting the codebase. The following section will cover some of the main reasons why the code was designed the way it is.

Organization

The code structure in PEACH follows a simple, layered structure adhering to best practice recommendations from Express, Node, and TypeScript coding styles. The application has the following discrete layers:

Layer Purpose
controllers Contains controller Express logic for determining what services to invoke and in what order
db Contains the direct database scaffolding, migrations, seeds, and connectivity logic
docs Contains the OpenAPI specification
middlewares Contains middleware functions for handling authorization, validation, and feature toggles
public Contains static and publicly accessible assets
repositories Contains standard table query builder definitions
routes Contains defined Express routes for defining the PEACH API shape and invoking controllers
services Contains logic for interacting with the database repository for specific tasks
types Contains the TypeScript types and domains to transform between
utils Contains useful utilities and functions that are generic and may be used across the codebase
validators Contains logic that examines and enforces incoming request shapes and patterns

Each layer is designed to focus on one specific aspect of business logic. Calls between layers are designed to be deliberate, scoped, and contained. This hopefully makes it easier to tell at a glance what each piece of code is doing and what it depends on. For example, the validation layer sits between the routes and controllers. It ensures that incoming network calls are properly formatted before proceeding with execution.

---
title: Network Request Flow Diagram
---

graph LR
    ST@{ shape: sm-circ, label: "Start" } --> routes
    routes --> middlewares
    middlewares --> validators
    validators --> controllers
    controllers --> services
    services --> repositories
    repositories --> EN@{ shape: fr-circ, label: "End" }
Loading

Middleware & Routing

PEACH middleware focuses on ensuring that the appropriate business logic filters are applied as early as possible. Concerns such as feature toggles, authentication, and authorization are handled here. Express executes middleware in the order of introduction. It will sequentially execute and then invoke the next callback as a part of its call stack. Because of this, we must ensure that the order we introduce and execute our middleware adheres to the following pattern:

  1. Ensure authentication and authorization is valid.
  2. Ensure that the contents, query, and params within the request are structurally sound.
  3. Ensure that the request contents contain only correct data.
  4. Perform any remaining middleware hooks prior to invoking the controller logic.

Controllers, Services, and Repositories

Our software stack follows the Controller-Service-Repository design pattern. In a nutshell, we separate out the levels of concern into the following:

  • Controllers manage the REST interface responses and invoke the necessary services to perform a business function.
  • Services focus on the business logic implementations and break down tasks into sequences of operations.
  • Repositories focus on storing the SQL queries and their associated query builders.

In summary, a Controller focuses on how to respond to the REST client, while the Service focuses on what business functions must occur. Repositories focus on assembling the SQL queries to perform operations but do not actually execute them. Instead, that is delegated to the Service to manage, as the Service knows the business logic and functions needed. You should consider the repository layer a glorified query builder layer that holds all of the recipes for doing the work, whereas the Service orchestrates all of the work to be done.

Types and Validators

The types layer solely contains TypeScript types and domains to transform between. It is not intended to contain any business logic, but rather to provide a common set of types that can be used across the codebase. This helps ensure type safety and consistency in how data is represented. If you are thinking about modifying some types, please ensure that you are not manually overriding any autogenerated types as there exists separate tooling to generate those definitions. Check the top of the file for comments on how to regenerate the types if necessary.

The validators layer contains logic that examines and enforces incoming request shapes and patterns. It is responsible for validating the structure of incoming requests, ensuring that they conform to the expected format before they are processed by the controllers. This helps catch errors early in the request lifecycle and provides a clear contract for what data is expected.

Validation leverages JSON Schema validation through the ajv library as well as some custom dictionary-based validation logic. This allows us to ensure that both the structure and the content of incoming requests are valid before they reach the business logic layer. The validators are designed to be reusable and composable, allowing for easy extension and modification as the API evolves.

Error Handling

Error handling in PEACH is designed to be consistent and informative. Any thrown errors will fall into one of two categories: known response errors, or unexpected errors. Depending on the type of error classification, we will handle the error differently.

For known response errors, we will leverage the Problem utility to generate a standardized error response compliant with RFC9457. This ensures that all known errors are handled in a consistent manner, providing clear and actionable feedback to the client. For all other unexpected errors, a simple and generic throw new Error() will cover the majority of cases, as these are typically indicative of bugs or issues that need to be addressed in the codebase. This approach allows us to differentiate between expected and unexpected errors, making it easier to debug and maintain the application.

Note

As a convention, only controllers and middlewares should throw Problem errors; the rest of the codebase should not throw Problem errors, and instead only throw generic errors. This helps ensure that we only expose and handle network request concerns at the appropriate layers, while keeping the rest of the codebase focused on business logic and data manipulation.

Optimization

PEACH is designed to be a performant and efficient microservice. We focus on optimizing the codebase in several key areas:

  1. Caching: We implement caching strategies to reduce the load on the database and improve response times. This involves using an in-memory LRU caching approach for frequently accessed table data, allowing us to bypass database queries for heavily used resources. This caching layer is designed to be hot-swappable in the application logic, allowing us to cache responses without requiring significant changes to the codebase. We outline more of the caching strategy in the LRU Cache Strategy section of the Design Principles document.

  2. Asynchronous Processing: We leverage asynchronous processing and query parallelization to minimize the impact of long-running transactions. Where appropriate, we leverage Promise.all() to allow multiple non-blocking database queries to be executed in parallel. This helps to reduce the overall response time for API requests and maximize DB pool utilization.

  3. Code Optimization: We continuously review and refactor the codebase to eliminate bottlenecks and improve overall efficiency. This includes optimizing algorithms, reducing complexity, improving abstractions and eliminating unnecessary computations where possible.

  4. Monitoring and Profiling: We use monitoring and profiling tools to identify performance issues and track the impact of optimizations. This data-driven approach allows us to make informed decisions about where to focus our optimization efforts.

By focusing on these areas, we aim to deliver a high-performance API that meets the needs of integration while preserving a high degree of availability and integrity.

Clone this wiki locally