N-tier layered architecture

With n-tier architecture, adopting new technologies and making development more efficient is easy. This layered architecture provides the flexibility to add new features in each layer without disturbing the features of other layers. In terms of security, you can keep each layer secure and isolated from the others, so if one layer gets compromised, the other layers won’t be impacted. Application troubleshooting and management also become manageable as you can quickly pinpoint where an issue is coming from and which part of the application needs to be troubleshot. 

The most common architecture in multilayer design is three-tier architecture.

  • Web Layer: The web layer is the user-facing part of the application. End users interact with the web layer to collect or provide information. 
  • Application Layer: The application layer mainly contains business logic and acts upon information received from the web layer. 
  • Database Layer: All kinds of user data and application data are stored in the database layer.

Multi-tenant architecture

ith each tenant being isolated by their unique configuration, identity, and data, they remain invisible to each other while sharing the same product. At the data access layer, each tenant will have data-level isolation with one of the following methods: 

  • Database-Level Isolation: In this model, each tenant has its database associated with its tenant ID. When each tenant queries data from the user interface, they are redirected to their database. This model is required if the customer doesn’t want a single shared database for compliance and security reasons. 
  • Table-Level Isolation: This isolation level can be achieved by providing a separate table for each tenant. In this model, tables need to be uniquely assigned to each tenant, for example, with the tenant ID prefix. When each tenant queries data from the user interface, they are redirected to their tables as per their unique identifier. 
  • Row-Level Isolation: All tenants share the same table in a database in this isolation level. There is an additional column in a table where a unique tenant ID is stored against each row. When an individual tenant wants to access their data from the user interface, the application’s data access layer formulates a query based on the tenant ID to the shared table. Each tenant gets a row that belongs to their users only.

Domain Driven Design

Domain-Driven Design (DDD) is a methodology and set of practices aimed at understanding and solving complexity at the heart of software. This approach is used to design and model software based on the “domain,” or the business’s core logic and key concepts. Using a common language and dividing the system into clear contexts, DDD promotes a deep understanding of the problem space and leads to a design that accurately reflects the underlying business needs. It’s particularly valuable in complex domains, where aligning the software closely with the real-world concepts it represents is vital. 

  • Domain: A “domain” refers to a specific problem area the software intends to address. The application logic revolves around the sphere of knowledge and activity. Understanding the domain is essential for creating a system that truly meets the needs of the business. For HMS, the domain will be healthcare management, focusing on patients, medical staff, appointments, treatments, and billing. Ubiquitous Language: 
  • Ubiquitous Language is a shared language between developers and non-technical stakeholders that describes the domain. This common language ensures that all team members understand the key terms and concepts in the same way, reducing misunder- standings and promoting clear communication for the HMS, creating a shared language that both medical professionals and developers understand, for example, Patient, Appointment, Treatment, Medical Staff, etc. 
  • Bounded contexts: In DDD, the application is divided into different bounded contexts, each representing a specific responsibility or functionality within the overall domain. A bounded context encapsulates all the terms, definitions, and logic for that specific part of the domain, and it is explicit about what is inside and outside its boundaries. For example, the Patient Management bounded context handles patient records, personal information, medical history, etc. An Appointment Scheduling bounded context includes managing appointments, sched- uling, cancellations, rescheduling, etc., and the Billing bounded context includes processing payments, insurance, invoices, etc. 
  • Entities: These objects have a distinct identity that runs through time and different states, for example, patients (with a unique ID) and medical staff (with unique credentials).
  • Value objects: Objects that describe characteristics of a thing but have no conceptual identity, They are immutable and can be easily replaced. For example, Address, Date of Birth, and Medical History (as these don’t have individual identities). 
  • Aggregates: An aggregate is a cluster of associated objects treated as a single unit for data chang es. One entity within the aggregate is the root, and external references are restricted to this root to ensure integrity. For example, in an online healthcare management system, a medical appointment can be modeled as an aggregate. The aggregate might include entities and value objects like Patient (who the appointment is for), Medical Staff (who will attend the patient Treatment Room (where the appointment will take place), and Time Slot (when the appoin ment is scheduled). Here, the Appointment entity would be the aggregate root. Any changes to the Patient, Medical Staff, Treatment Room, or Time Slot related to a specific appointment would be made through the Appointment entity. This ensures that the appointment aggregat maintains consistency and enforces all business rules related to medical appointments. 
  • Repositories: Repositories are used to retrieve aggregates from the underlying persistence layer. They provide an abstraction allowing the rest of the application to interact with the data store in a way consistent with the domain model. For example, the Patient repository is used to fetch and manage Patient entities, and the Appointment repository is used to retrieve and store Appointment aggregates. 
  • Factories: Factories are responsible for encapsulating the logic of creating complex objects and aggregates. They ensure that an object or aggregate is created in a consistent and valid state. For example, the Patient factory is used to create a new Patient entity with a valid initial state, and the Appointment factory is used to create a new Appointment aggregate with the required details. 
  • Services: When an operation doesn’t logically belong to a value object or entity, it can be defined as a service. Services are part of the domain model and contain business logic that operates on the domain’s concepts. For example, in the Billing context, the billing service contains op- erations like calculating total charges, applying insurance discounts, generating invoices, etc. 
  • Domain events: Domain events capture the fact that something significant has happened within the domain. They can trigger other activities within the system or in other systems. For example, an appointment scheduled event triggered when a new appointment is scheduled may notify relevant staff members and a payment processed event occurs after successful payment, which might initiate a receipt generation process. 
  • Anti-corruption layer: This layer translates between different parts of the system that use different languages or models. It ensures that each model’s integrity is maintained, and inconsistencies are handled. If the Billing system must interact with an external third-party payment gateway, an anti-corruption layer could translate between the domain model in the Billing context and the model used by the external system.

Circuit Breaker Pattern

It’s common for a distributed system to make a call to other downstream services, and the call could fail or hang without a response. A retry function would consume threads and potentially induce a cascading failure. The circuit breaker pattern is about understanding the health of downstream dependencies. It detects when those dependencies are unhealthy and implements logic to gracefully fail requests until it detects that they are healthy again. 

The implementation decisions involve 

  1. the state machine tracking/sharing
  2. the healthy/unhealthy request counts 

The states of services can be maintained in DynamoDB, Redis/Memcached, or another low-latency persistence store.

If a defined percentage of requests observe an unhealthy behavior or a total count of exceptions, regardless of percentage, the circuit is marked as open. All requests throw exceptions rather than integrate with the dependency for a defined timeout period. Once the timeout period has subsided, a small percentage of requests try integrating with the downstream dependency to detect when the health has returned. Once a sufficient percentage of requests are healthy again over an interval, or no errors are observed, the circuit closes again, and all the requests are allowed to thoroughly integrate as they usually would.

Bulkhead Pattern

Bulkheads are structural partitions used in ships to create individual watertight sections. The primary purpose is to contain the consequences of any breach in the ship’s hull. This design aims to minimize the risk of the entire ship sinking if one area is compromised.

The same concept is helpful to limit the scope of failure in the architecture of large systems where you want to partition your system to decouple dependencies between services. The idea is that one failure should not cause the entire system to fail

In the bulkhead pattern, it’s better to isolate the service of the application which has a high dependency into service pools. If one pool fails, not all services depending on that service fails, on the ones utilizing the pool that failed. Other services continue to serve upstream services. 

The following are the significant points to consider when introducing the bulkhead pattern in your design, especially for the shared service model: 

  • Save part of the ship, which means your application should not shut down due to the failure of one service. 
  • Decide whether less efficient use of resources is okay. Performance issues in one partition should be fine for the overall application. 
  • Pick a useful granularity. Make sure to make the service pools manageable; make sure they can handle the application load. 
  • Monitor each service partition performance and adhere to the SLA. Ensure all moving parts are working together and test the overall application when one service pool is down. 

You should define a service partition for each business or technical requirement. It would be best if you used this pattern to prevent the application from cascading failure and isolating critical consumers from the standard consumer. 

Database handling

Data is always central to any application development, and scaling data has always been challenging. You can put either a Memcached or Redis cache in front of your database, reducing the many hits on the database and improving database latency. As you need to handle more data with your relational database, you need to add more storage or vertically scale the database server by adding more memory and CPU power. Often, horizontal scaling is more complex when it comes to scaling relational databases. 

If your application is read-heavy, you can achieve horizontal scaling by creating a read replica. Route all read requests to database read replicas while keeping the master database node to serve write and update requests. As a read replica has asynchronous replication, it can add some lag time. You should choose the read replica option if your application can tolerate some milliseconds of latency. You can use read replicas to offload reporting. 

You can use database sharding to create a multi-master for your relational database and inject the concept of horizontal scaling. The sharding technique is used to improve writing performance with multiple database servers. The database is structured and segmented into identical sections, with appropriate table columns serving as keys for distributing the writing processes.

HA Pattern

For the high availability of your application, it is critical to keep your database up and running all the time. To achieve high database availability, you can have a standby replica of the master database instance.

A read replica takes the load off the primary instance to handle latency. The primary and standby are located in different availability zones, so your application will still be up even when an entire availability zone is down. This architecture also helps to achieve zero downtime, which may be caused during the database maintenance window. When a primary instance is down for maintenance, the application can fail over to a secondary standby instance and continue serving user requests. 

For disaster recovery, you will want to define the database backup and archival strategy, depending on your application’s recovery point objective (RPO) of how frequently you want to take backups. 

If your RPO is 30 minutes, it means your organization can only tolerate 30 minutes’ worth of data loss. In that case, you should take a backup every half an hour. While storing the backup, you need to determine how long the data can be stored for customer query purposes. You can store data for six months as an active backup and then in an archival store as per the compliance requirement. 

Consider how quickly you need to access your backup and determine the type of network connection needed to meet your backup and recovery requirements as per the company’s recovery time objective (RTO). 

Depending on your application’s growth and complexity, consider migrating to a NoSQL database. NoSQL can provide greater scalability management, performance, and reliability.

Clean Architecture

Clean Architecture, also known as Hexagonal Architecture emphasizes the separation of concerns, maintainability, and testability. Clean Architecture aims to create a flexible, adaptable, and maintainable system over time. 

Clean Architecture divides your application into five key components: 

  1. Entities (innermost layer): Entities are the business objects that encapsulate the core business rules. They are independent of any specific technology, database, or framework. Entities repre sent the “things” in the system and what they can do. 
  2. Use cases: Use cases contain the application-specific rules and define how the entities interact to fulfill specific scenarios or user stories. They coordinate the flow of data and actions between entities and external interfaces. They are also technology-agnostic, focusing only on business logic. A checkout use case might involve validating the shopping cart, applying discounts, calculating shipping, and processing payment, for example. 
  3. Interfaces (ports): Interfaces define contracts for how different layers of the system interact with each other. They create a boundary that separates the inner layers (entities and use cases) from the outer layers (adapters, frameworks, and drivers). This separation enables flexibility and maintainability. There might be an interface for payment processing that defines methods like processing payments and refunds. 
  4. Adapters: Adapters implement the interfaces and translate between the inner and outer layers. They allow the application to interact with external components like databases, APIs, or third-party libraries. Adapters allow the core logic to remain isolated from technological changes or external dependencies. A database adapter might implement a data access interface to handle interaction with a specific database technology.
  5. Frameworks and drivers (outermost layer): This layer comprises all the technical details and tools used to build the application. It includes web servers, databases, UI frameworks, third-party libraries, etc. This layer interacts with the adapters to connect the core application to the outside world. This could include implementing a RESTful API using a specific web framework, setting up a connection to an SQL database, or integrating with a third-party payment gateway. 

In Clean Architecture, each layer is independent of the others, allowing changes in one layer without affecting the others. You can switch databases, change the UI framework, or modify business logic without causing ripple effects throughout the system. Since your architecture has well-defined interfaces, it’s easier to create mocks or stubs for testing. Core business logic can be tested independently from databases, UI, or other external dependencies. While using Clean Architecture, make sure to avoid over-engineering. For simple or small projects, the complexity and overhead of Clean Architecture might need to be revised. 

Clean Architecture provides a robust and flexible foundation for developing software that can adapt to changing technologies and requirements. Focusing on separating concerns and clear boundaries between layers promotes maintainability, scalability, and testability. It’s a robust pattern that can serve well in complex systems but must be applied with an understanding of the needs and context of the specific project to avoid unnecessary complexity.

Anti-patterns

Try to and avoid the following architecture design anti-patterns.

Scaling is handled reactively and manually. It’s only when users start reporting issues that the administrator becomes aware of the problem. The admin then initiates the process of launching a new server instance to alleviate the load on existing servers. 

Remedy: You should take a proactive approach and use auto-scaling. With anti-patterns, automation is missing. Automating the detection of unhealthy resources and launching replacement resources can streamline operations. Furthermore, it’s possible to implement automated notifications when such resource changes occur.

The server is kept for a long time with hardcoded IP. Server configurations can become inconsistent leading to the inefficient allocation of resources, with some resources running when they are not needed. 

Remedy: It would help if you kept all of the servers identical and had the ability to switch to a new Ip address. You should automatically terminate any unused resources. 

An application is built monolithically, where all layers of the architecture, including web, applicablind data layers, are tightly coupled and server-dependent. If one server crashes, it brings down the entire application. 

Remedy: Keep the application and web layers independent by adding a load balancer in between. In the event that one of the application independent by adding a load balanced balancer automatically redirects all traffic to the re maining healthy servers. 

The application is server-bound, and the servers communicate directly with each other. User authentication and sessions are stored in the server locally, and all static files are served from the local server. 

Remedy: You should create a service-oriented RESTful architecture, where the services talk to each other using a standard protocol such as HTTP. User authentication and sessions should be stored in low-latency distributed storage to scale the application horizontally. The static asset should be stored in centralized object storage decoupled from the server. 

A single database is used for all kinds of needs. You use a relational database for all needs, which introduces performance and latency issues. 

Remedy: You should use the right storage for the right need, such as the following: 

  • NoSQL to store the user session 
  • Cache data storage for low-latency data availability 
  • Data warehouse for reporting needs 
  • Relational database for transactional data 

A single point of failure by having a single database instance to serve the application. 

Remedy: Whenever feasible, remove single points of failure from your architecture. Establish a secondary server (standby) and replicate the data. In the event of a primary database server failure, the secondary server can take over the workload. 

Static content is served directly from the server without caching. 

Remedy: It would be best if you considered using a CDN to cache heavy content near the user location, which helps to improve page latency and reduce page load time. 

Security loopholes that open server access without a finegrained security policy. 

Remedy: You should always apply the principle of least privilege, which means starting with no access and only giving access to the required user group.

Leave a Reply

Your email address will not be published. Required fields are marked *