Basic Application Architecture
Topics covered:
- Monolithic Architecture
- Layered architecture and separation of concerns
- REST API design
- Database modelling and SQL
Important This won't be a deep down architecture and design discussion. Moreover treat it like a high level overview.
Monolithic Architecture
In Monolithic Architecture, an application is built as a single unit with all its components, including the user interface, business logic, and data access, all compiled and deployed together. The result is a single codebase that produces one deployable artifact, like a JAR or WAR file in Java, a Docker image, or a simple folder structure. These share a common runtime process and usually a single database.
When making any changes to a monolithic system, be it small tweaks or fixing bugs, the entire application needs to be rebuilt, tested, and redeployed through one pipeline. Also their's a factor of single point of failure.
Google Mail, Google Maps, Google Search are all monolithic. Google is renowned for it's use of Monolithic architecture, when other organizations have shifted to Microservices. Other notable example is Linux Kernal.
Two reasons why monolith still preffered:
- Simplicity
- It works for our use case, don't change it.
Modern advice is often: Start with a monolith, design modularly, and split into services only if and when you need to.
Layered Architecture
We use Layered architecture to organise a monolithic application. The idea is to split the system into horizontal layers, each with a clear responsibility.
You can refer to this video for more information: YouTube
A classic three-layer architecture looks like this:
- Presentation layer: Handles external interactions: HTTP APIs, web UI, CLI, etc. Most common responsibilities include request parsing, input validation, response formatting, routing.
- Application Layer: Contains business rules and use cases. Orchestrate workflows, domain logic, validation that crosses entities, integration with external services.
- Data Access Layer: Manages persistence and retrieval of data. Responsibilities include database queries, ORM mapping, repositories, and translation between domain objects and database tables.
Each layer depends only on the layer directly beneath it. For example, the presentation layer should not bypass the application layer and talk directly to the database; it should instead invoke the application layer, which in turn uses the data layer.
There are few advantages to layered architecture, including it's structured design, separation of concerns, testability, and maintainability. For far us to talk that, once the interface remains stable we can scale the architecture.
Separation of Concerns and Modular Design
Separation of concerns is a design principle that states different responsibilities should be separated into different modules, and ideally, into different parts of the architecture. Within each layer, modular design extends the separation of concerns by breaking code into smaller, focused modules. These modules can be organized by feature, such as {orders, users, or payments}, with each module containing its own presentation, business, and data logic while staying within layer boundaries. Modules can also be divided by technical role, like controllers, services, or repositories, which are nested in their proper layers. This approach makes the system easier to understand, as it is composed of manageable, self-contained pieces.
This approach also enables practical upgrades, such as swapping or evolving one module without overhauling the entire app. It allows easy transformation of monolith into microservice in the future.
REST API Design Principles
REST, or Representational State Transfer, is an architectural style for designing web services and APIs that can handle a large volume of users. This concept was first introduced by Roy Fielding in his doctoral dissertation in 2000. A REST API is essentially a collection of guidelines that dictate how different software systems, such as a mobile app and a server, can interact with each other over the internet. They do this by using standard web protocols, like HTTP. This allows for a relatively straightforward exchange of information between systems.
Core Constraints
Client-server separation lets frontends and backends evolve independently. Since the system is stateless, each request contains all the information needed to complete it, so there's no need to store session information on the server. This makes it easier to predict how the system will behave when it's handling a lot of requests. Caching also helps with performance by storing and reusing responses. The system's layered design means that components like proxies and load balancers are invisible to clients, which adds to the system's flexibility. In some cases, servers can even send code to clients on demand. What holds everything together is a uniform interface, where resources are identified by URIs, manipulated using standard HTTP methods, and represented in formats such as JSON.
URI Design Guidelines
- Use plural nouns to identify collections, such as
/ordersor/customers. - Identify specific items with path parameters, for example
/orders/123. - Organize URIs in a hierarchical and predictable manner, such as
/customers/42/ordersto access the orders of customer 42. - Refrain from using verbs in paths.
HTTP Methods - Richardson Maturity Model Level 2
The Richardson Maturity Model measures RESTfulness, with Level 2 emphasizing proper HTTP verbs:
- GET
/orders/123: Retrieves a resource. Safe (no side effects) and idempotent (repeatable). - POST
/orders: Creates a new resource (server assigns URI, e.g.,/orders/123). Non-idempotent. - PUT
/orders/123: Replaces or creates a resource at a client-specified URI. Idempotent. - DELETE
/orders/123: Removes a resource. Idempotent.
Their is also PATCH which is similar to updating specific data. Example flow for an order: POST to /orders creates it; GET /orders/123 fetches details; PUT /orders/123 updates fully; DELETE /orders/123 removes it. This design promotes scalability, as servers stay simple and stateless while clients drive interactions.
Handling errors and status codes
Handling errors and status codes is crucial for effective communication. HTTP status codes indicate the outcome of a request, with 2xx codes signifying success, such as 200 OK, 201 Created, or 204 No Content. Client errors are represented by 4xx codes, including 400 Bad Request, 404 Not Found, and 409 Conflict. Server errors are indicated by 5xx codes, like 500 Internal Server Error.
A well-designed API should also include clear error payloads, such as {"error": "Invalid argument", "details": "..."}. API versioning, for example /v1/orders, allows for backward-compatible changes.
Which layer REST is present?
In a layered application, the presentation layer is where REST controllers are need to be present. These controllers parse incoming requests, call the business layer, and convert results into appropriate HTTP responses.
Basic Database Modeling and SQL
A database is an organized collection of data or a type of data store based on the use of a database management system (DBMS), the software that interacts with end users, applications, and the database itself to capture and analyze the data. Wikipedia
Entities, tables, and relationships
In a relational model, key entities like Customer, Order, or Product are identified. A dedicated table is built for each of these entities, with columns for attributes such as name, email, or created_at. Each row in a table is uniquely identified by a primary key. Foreign keys establish relationships between tables, which helps maintain data integrity and allows for efficient queries.
Here, customer_id is the primary key for the given table. Therefore, this becomes a foreign key for another table.
| Column Name | Data Type | Constraints | Description |
|---|---|---|---|
| customer_id | SERIAL | PRIMARY KEY | Unique identifier for customer |
| name | VARCHAR(100) | NOT NULL | Customer's full name |
| VARCHAR(255) | NOT NULL, UNIQUE | Customer's email address | |
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp |
Relationships in relational databases show how data connects. There are three basic types, which can be explained using everyday shopping logic: one-to-one, one-to-many, and many-to-many.
A one-to-one relationship is when one item links to exactly one other, like a customer having one unique profile. This is modeled by a shared primary key or an exclusive foreign key in either table. In a one-to-many relationship, one entity owns many others. For example, one customer can place many orders. To model this, the primary key from the "one" side, such as customer_id, is placed as a foreign key in the orders table. This lets one customer reference multiple rows. A many-to-many relationship is when multiple entities link both ways, like many orders containing many products. To model this, a junction table, such as order_items, is created with foreign keys for both, order_id and product_id. This forms a bridge between the two tables without direct links.
Normalization: reducing redundancy
Normalization streamlines the database by cutting redundancy and messy dependencies, making data easier to maintain and query. These are;
- First Normal Form (1NF) eliminate repeating groups, ensuring every column holds atomic values, no lists jammed into cells.
- Second Normal Form (2NF) builds on 1NF by eliminating partial dependencies. Every non-key column must rely on the whole primary key, not just part of it, which is important for composite keys.
- Third Normal Form (3NF) goes further, banning transitive dependencies where non-key columns depend on other non-key columns. In 3NF, everything non-key answers only to the primary key.
Basic SQL
SQL is the standard language for querying and manipulating relational databases.
Key operations:
- SELECT: Retrieve data.
- INSERT: Add new rows.
- UPDATE: Modify existing rows.
- DELETE: Remove rows.
- CREATE TABLE: Define tables and constraints.
-- Find all orders for a customer
SELECT order_id, order_date, total
FROM orders
WHERE customer_id = 42;
-- Insert a new order
INSERT INTO orders (customer_id, order_date, total)
VALUES (42, CURRENT_DATE, 100.00);
-- Update an order's status
UPDATE orders
SET status = 'shipped'
WHERE order_id = 123;
-- Delete a specific order
DELETE FROM orders
WHERE order_id = 123;