Three tier architecture
Key takeaways
Three-tier architecture | Software application architecture that organizes applications into three logical tiers. |
Inversion of control | Design principle used to invert control in OO programming to achieve loose coupling. |
Factory | A method used to define a separate operation for object creation. |
Locator pattern | A pattern that considers a registry known as the "service locator", which stores the information necessary to perform a certain task. |
Configuration file | A YAML file containing information about the different components and their configurations. |
Introduction
In this tutorial, you will learn how to construct an application using Pip.Services components and a three-tier structure.
We will begin with a brief description of the example that we will be constructing and a list of the necessary pre-requisites.
Then, we will see a detailed description of the three tiers with code examples for each of them. We will continue by explaining how two important concepts are applied in Pip.Services: inversion of control and locator pattern, and how to construct a process container for our program.
Next, we will see how to run our app by selecting a specific database, and how the results obtained from its execution are presented on our browser.
We will finish by showing the complete code of our example and summarizing what was learned.
Brief description of the example
The example in this tutorial consists of an application that sends a message to a browser. The message has the format “Hello {name}!” where name is the random name of a person that was selected from a database.
In order to achieve this, we divide our app into three tiers. The first is the presentation or view layer, which consists of a REST service that will provide information to the browser. The second is the application layer. This tier contains a controller that connects the REST service to the database and extracts a random name from it. The last one is the data or persistence layer, which is created by using a MySQL database. The following table summarizes this and the concepts behind.
Name(s) of the tier | Function | Example |
---|---|---|
- Presentation layer - View - Controller |
- Endpoints: expose the microservice to external consumers - There can be more than one endpoint |
HTTP/- Controller - Presents the message “Hello {name}” |
- Application layer - Service |
- Core business logic | - Obtains a random name from the database |
- Data layer |
- Persistence layer | - Data storage |
Pre-requisites
Before creating this app, we need to install several modules that contain the necessary components. They are:
npm install pip-services4-container-node --save
pip install pip-services4-container
npm install pip-services4-mysql-node --save
pip install pip-services4-mysql
npm install pip-services4-components-node --save
pip install pip-services4-components
npm install pip-services4-http-node --save
pip install pip-services4-http
npm install pip-services4-data-node --save
pip install pip-services4-data
Data object
In order to use the data obtained from the database, we define a data structure that mirrors the table where the data is stored.
This table contains three columns of type varchar, namely id, type, and name. Thus, our data structure looks like this:
import { IStringIdentifiable } from "pip-services4-data-node";
export class MyFriend implements IStringIdentifiable {
public id: string;
public type: string;
public name: string;
}
from pip_services4_data.data import IStringIdentifiable
class MyFriend(IStringIdentifiable):
def __init__(self, id: str, type: str, name: str):
self.id = id
self.type = type
self.name = name
Tier 1: Presentation layer or view
This layer is used to show the result of our app on the browser. It is constructed as a subclass of the RestService class. In it, we set a reference to the controller to create the connection between the two and be able to use the greetings() method. We also define the elements of the URL to the resulting webpage.
export class HelloFriendRestController extends RestController {
protected service: HelloFriendService;
public constructor() {
super()
this._baseRoute = "/hello_friend";
}
public configure(config: ConfigParams): void {
super.configure(config);
}
public setReferences(references: IReferences): void {
super.setReferences(references);
this.service = references.getOneRequired(new Descriptor("hello-friend", "service", "*", "*", "1.0"));
}
private async greeting(req: any, res: any): Promise<void> {
let result = await this.service.greeting();
await this.sendResult(req, res, result);
}
private async create(req: any, res: any): Promise<void> {
let correlationId = this.getCorrelationId(req);
let friend: MyFriend = req.query;
let result = await this.service.create(correlationId, friend);
await this.sendResult(req, res, result);
}
public register(): void {
this.registerRoute("GET", "/greeting", null, this.greeting);
this.registerRoute("GET", "/greeting_create", null, this.create);
}
}
import bottle
from pip_services4_data.data import IStringIdentifiable
from pip_services4_data.validate import Schema
from pip_services4_http.controller import RestController
class HelloFriendRestController(RestController):
def __init__(self):
super(HelloFriendRestController, self).__init__()
self._base_route = "/hello_friend"
self._service: HelloFriendService = None
def configure(self, config):
super().configure(config)
def set_references(self, references):
super(HelloFriendRestController, self).set_references(references)
self._service = references.get_one_required(Descriptor('hello-friend', 'service', '*', '*', '1.0'))
def register(self):
self.register_route(method="GET", route="/greeting", schema=Schema(), handler=self.greeting)
self.register_route(method="GET", route="/greeting_create", schema=Schema(), handler=self.create)
def greeting(self):
result = self._service.greeting()
return self.send_result(result)
def create(self):
trace_id = self._get_trace_id()
item = MyFriend(
bottle.request.query["id"],
bottle.request.query["type"],
bottle.request.query["name"]
)
result = self._service.create(correlation_id, item)
return self.send_result(result)
Tier 2: Application layer or controller
The controller allows us to connect the presentation and persistence layers and produce some data transformations.
Thus, it sets a reference to the database. This reference is not to a specific database, but a general persistence component that will allow us to select between different databases at deployment time.
This class also defines the greeting method, which selects a random name from the database and then passes it to the view. It also defines a default name, which will be used if no name is obtained from the database query.
export class HelloFriendService implements IConfigurable, IReferenceable {
private defaultName: string;
private persistence: HelloFriendPersistence;
public constructor() {
this.defaultName = "Pip User";
}
public configure(config: ConfigParams): void {
this.defaultName = config.getAsStringWithDefault("default_name", this.defaultName);
}
public setReferences(references: IReferences): void {
this.persistence = references.getOneRequired(new Descriptor("hello-friend", "persistence", "*", "*", "1.0"));
}
public async greeting(): Promise<string> {
let filter = FilterParams.fromTuples("type", "friend");
let selectedFilter = await this.persistence.getOneRandom(null, filter);
let name = selectedFilter != null ? selectedFilter.name : null;
return `Hello, ${name} !`;
}
public async create(ctx: Context, item: MyFriend): Promise<MyFriend> {
let res = await this.persistence.create(ctx, item);
return res;
}
}
from pip_services4_components.config import IConfigurable
from pip_services4_components.refer import IReferences, IReferenceable
from typing import Optional
class HelloFriendService(IConfigurable, IReferenceable):
__defaultName = None
__persistence: 'HelloFriendPersistence' = None
def __init__(self):
self.__defaultName = "Pip User"
def configure(self, config):
self.__defaultName = config.get_as_string_with_default("default_name", self.__defaultName)
def set_references(self, references: IReferences):
self.__persistence = references.get_one_required(Descriptor("hello-friend", "persistence", "*", "*", "1.0"))
def greeting(self):
filter_param = FilterParams.from_tuples("type", "friend")
selected_friend = self.__persistence.get_one_random(None, filter_param)
name2 = selected_friend.name
return f"Hello, {name2} !"
def create(self, trace_id: Optional[str], item: MyFriend) -> MyFriend:
res = self.__persistence.create(trace_id, item)
return res
Tier 3: Data layer or persistence layer
This layer connects to a database containing a table with names. The class constructor accepts the name of the table to be used, which in this example is called ‘myfriends’.
The class also contains the defineSchema() method, which ensures that if our table doesn’t exist in the database, it is created.
Next, it contains the composeFilter() method, which customizes a filter to the needs of the database, and the getOneRandom() method, which is an override of the parent class.
export class HelloFriendPersistence extends IdentifiableMySqlPersistence<MyFriend, string> {
public constructor() {
super("myfriends3");
}
protected defineSchema(): void {
this.clearSchema();
this.ensureSchema('CREATE TABLE IF NOT EXISTS `' + this._tableName + '` (id VARCHAR(32) PRIMARY KEY, `type` VARCHAR(50), `name` TEXT)');
}
private composeFilter(filter: FilterParams): string {
filter ??= new FilterParams();
let type = filter.getAsNullableString("type");
let name = filter.getAsNullableString("name");
let filterCondition = "";
if (type != null)
filterCondition += "type='" + type + "'";
if (name != null)
filterCondition += "name='" + name + "'";
return filterCondition;
}
public getOneRandom(ctx: Context, filter: any): Promise<MyFriend> {
return super.getOneRandom(ctx, this.composeFilter(filter));
}
}
from pip_services4_mysql.persistence import IdentifiableMySqlPersistence
from pip_services4_data.query import FilterParams
class HelloFriendPersistence(IdentifiableMySqlPersistence):
def __init__(self):
super(HelloFriendPersistence, self).__init__('myfriends3')
def _define_schema(self):
self._clear_schema()
self._ensure_schema(
'CREATE TABLE IF NOT EXISTS `' + self._table_name + '` (id VARCHAR(32) PRIMARY KEY, `type` VARCHAR(50), `name` TEXT)')
def _compose_filter(self, filter: FilterParams):
filter = filter or FilterParams()
type = filter.get_as_nullable_string('type')
name = filter.get_as_nullable_string('name')
filter_condition = ''
if type is not None:
filter_condition += "`type`='" + type + "'"
if name is not None:
filter_condition += "`name`='" + name + "'"
return filter_condition
def get_one_random(self, correlation_id: str, filter: FilterParams) -> MyFriend:
return super().get_one_random(trace_id, self._compose_filter(filter))
Containerization
Now that we have the code for our three tiers, we can put it together in an executable container. This is done in two steps: object creation and binding.
The first is based on the inversion of control principle through the use of factories. The second considers the Locator pattern through an external configuration file with information on the different modules and their properties. The following sections explain them in detail.
Inversion of control: Factories
Pip.Services uses the Inversion of Control principle to create different objects. As such, it employs factories to create instances of classes.
In our example, we create the HelloFriendServiceFactory, which is a subclass of Factory and registers the HelloFriendRestService, HelloFriendController, and HelloFriendPersistence components as classes to be instantiated.
export class HelloFriendServiceFactory extends Factory {
public constructor() {
super();
let HttpControllerDescriptor = new Descriptor("hello-friend", "controller", "http", "*", "1.0"); // Controller
let ServiceDescriptor = new Descriptor("hello-friend", "service", "default", "*", "1.0"); // Service
let PersistenceDescriptor = new Descriptor("hello-friend", "persistence", "mysql", "*", "1.0"); // Persistence
this.registerAsType(HttpControllerDescriptor, HelloFriendRestController); // Controller
this.registerAsType(ServiceDescriptor, HelloFriendService); // service
this.registerAsType(PersistenceDescriptor, HelloFriendPersistence); // Persistence
}
}
from pip_services4_components.refer import Descriptor
from pip_services4_components.build import Factory
class HelloFriendControllerFactory(Factory):
def __init__(self):
super(HelloFriendControllerFactory, self).__init__()
HttpControllerDescriptor = Descriptor('hello-friend', 'controller', 'http', '*', '1.0') # View
ServiceDescriptor = Descriptor('hello-friend', 'service', 'default', '*', '1.0') # Service
PersistenceDescriptor = Descriptor('hello-friend', 'persistence', 'mysql', '*', '1.0') # Persistence
self.register_as_type(HttpControllerDescriptor, HelloFriendRestController) # View
self.register_as_type(ServiceDescriptor, HelloFriendService) # Controller
self.register_as_type(PersistenceDescriptor, HelloFriendPersistence) # Persistence
Locator pattern: config file
Pip.Services uses the locator pattern to create the bindings between the different objects. To do this, we create a configuration file with information about the different components. Among them, we specify the actual configuration of our MySQL database.
---
# Container context
- descriptor: "pip-services:context-info:default:default:1.0"
name: "hello-friend"
description: "HelloFriend microservice"
# Console logger
- descriptor: "pip-services:logger:console:default:1.0"
level: "trace"
# Performance counter that post values to log
- descriptor: "pip-services:counters:log:default:1.0"
# Service
- descriptor: "hello-friend:service:default:default:1.0"
default_name: "Friend"
# Shared HTTP Endpoint
- descriptor: "pip-controller:endpoint:http:default:1.0"
connection:
protocol: http
host: 0.0.0.0
port: {{HTTP_PORT}}{{#unless HTTP_PORT}}8085{{/unless}}
# HTTP Service V1
- descriptor: "hello-friend:controller:http:default:1.0"
# Heartbeat service
- descriptor: "pip-controller:heartbeat-controller:http:default:1.0"
# Status service
- descriptor: "pip-controller:status-controller:http:default:1.0"
# Persistnece - MySQL
- descriptor: "hello-friend:persistence:mysql:default:1.0"
connection:
host: 'localhost'
port: '3306'
database: 'pip'
credential:
username: 'root'
password: ''
Process container
Now that our support structure has been created, we add the components to a process container. This container will allow us to run our code as a sole app.
export class HelloFriendProcess extends ProcessContainer {
public constructor() {
super("hello-friend", "HelloFriend microservice");
this._configPath = "./config.yaml"
this._factories.add(new HelloFriendServiceFactory());
this._factories.add(new DefaultHttpFactory());
}
}
from pip_services4_container.container import ProcessContainer
from pip_services4_http.build import DefaultRpcFactory
class HelloFriendProcess(ProcessContainer):
def __init__(self):
super(HelloFriendProcess, self).__init__('hello-friend', 'HelloFriend microservice')
self._config_path = './config.yaml'
self._factories.add(HelloFriendControllerFactory())
self._factories.add(DefaultRpcFactory())
Running the app
Our final step is to execute the app via the container’s run() command. The following example shows how to do this.
export async function main() {
try {
let proc = new HelloFriendProcess();
proc.run(process.argv);
} catch (ex) {
console.error(ex);
}
}
if __name__ == '__main__':
runner = HelloFriendProcess()
print("run")
try:
runner.run()
except Exception as ex:
print(ex)
Result
Once our app is running, we can see the results by calling the previously defined link. In our example, the URL is:
http://localhost:8080/hello_friend/greeting
And, if everything went right, we will see something similar to:
Complete code
Below, we can see the complete code of our example.
Code Example
import {
ConfigParams, Descriptor, IConfigurable,
IReferenceable, IReferences, Context, Factory
} from "pip-services4-components-node";
import { ProcessContainer } from "pip-services4-container-node";
import { RestController, DefaultHttpFactory } from "pip-services4-http-node";
import { IdentifiableMySqlPersistence } from "pip-services4-mysql-node";
import { IStringIdentifiable, FilterParams } from "pip-services4-data-node";
// Data object
export class MyFriend implements IStringIdentifiable {
public id: string;
public type: string;
public name: string;
}
// Tier 1: Controller
export class HelloFriendRestController extends RestController {
protected service: HelloFriendService;
public constructor() {
super()
this._baseRoute = "/hello_friend";
}
public configure(config: ConfigParams): void {
super.configure(config);
}
public setReferences(references: IReferences): void {
super.setReferences(references);
this.service = references.getOneRequired(new Descriptor("hello-friend", "service", "*", "*", "1.0"));
}
private async greeting(req: any, res: any): Promise<void> {
let result = await this.service.greeting();
await this.sendResult(req, res, result);
}
private async create(req: any, res: any): Promise<void> {
let correlationId = this.getCorrelationId(req);
let friend: MyFriend = req.query;
let result = await this.service.create(correlationId, friend);
await this.sendResult(req, res, result);
}
public register(): void {
this.registerRoute("GET", "/greeting", null, this.greeting);
this.registerRoute("GET", "/greeting_create", null, this.create);
}
}
// Tier 2 : Service
export class HelloFriendService implements IConfigurable, IReferenceable {
private defaultName: string;
private persistence: HelloFriendPersistence;
public constructor() {
this.defaultName = "Pip User";
}
public configure(config: ConfigParams): void {
this.defaultName = config.getAsStringWithDefault("default_name", this.defaultName);
}
public setReferences(references: IReferences): void {
this.persistence = references.getOneRequired(new Descriptor("hello-friend", "persistence", "*", "*", "1.0"));
}
public async greeting(): Promise<string> {
let filter = FilterParams.fromTuples("type", "friend");
let selectedFilter = await this.persistence.getOneRandom(null, filter);
let name = selectedFilter != null ? selectedFilter.name : null;
return `Hello, ${name} !`;
}
public async create(ctx: Context, item: MyFriend): Promise<MyFriend> {
let res = await this.persistence.create(ctx, item);
return res;
}
}
// Tier 3 = Persistence
export class HelloFriendPersistence extends IdentifiableMySqlPersistence<MyFriend, string> {
public constructor() {
super("myfriends3");
}
protected defineSchema(): void {
this.clearSchema();
this.ensureSchema('CREATE TABLE IF NOT EXISTS `' + this._tableName + '` (id VARCHAR(32) PRIMARY KEY, `type` VARCHAR(50), `name` TEXT)');
}
private composeFilter(filter: FilterParams): string {
filter ??= new FilterParams();
let type = filter.getAsNullableString("type");
let name = filter.getAsNullableString("name");
let filterCondition = "";
if (type != null)
filterCondition += "type='" + type + "'";
if (name != null)
filterCondition += "name='" + name + "'";
return filterCondition;
}
public getOneRandom(ctx: Context, filter: any): Promise<MyFriend> {
return super.getOneRandom(ctx, this.composeFilter(filter));
}
}
// Inversion of control: Factory
export class HelloFriendServiceFactory extends Factory {
public constructor() {
super();
let HttpControllerDescriptor = new Descriptor("hello-friend", "controller", "http", "*", "1.0"); // Controller
let ServiceDescriptor = new Descriptor("hello-friend", "service", "default", "*", "1.0"); // Service
let PersistenceDescriptor = new Descriptor("hello-friend", "persistence", "mysql", "*", "1.0"); // Persistence
this.registerAsType(HttpControllerDescriptor, HelloFriendRestController); // Controller
this.registerAsType(ServiceDescriptor, HelloFriendService); // service
this.registerAsType(PersistenceDescriptor, HelloFriendPersistence); // Persistence
}
}
// Containerization
export class HelloFriendProcess extends ProcessContainer {
public constructor() {
super("hello-friend", "HelloFriend microservice");
this._configPath = "./config.yaml"
this._factories.add(new HelloFriendServiceFactory());
this._factories.add(new DefaultHttpFactory());
}
}
// Running the app
export async function main() {
try {
let proc = new HelloFriendProcess();
proc.run(process.argv);
} catch (ex) {
console.error(ex);
}
}
Code Example
Code Example
Code Example
# Data object
from typing import Optional
import bottle
from pip_services3_commons.data import IStringIdentifiable, FilterParams
class MyFriend(IStringIdentifiable):
def __init__(self, id: str, type: str, name: str):
self.id = id
self.type = type
self.name = name
from pip_services3_commons.validate import Schema
from pip_services3_rpc.services import RestService
# Tier 1: View
class HelloFriendRestService(RestService):
def __init__(self):
super(HelloFriendRestService, self).__init__()
self._base_route = "/hello_friend"
self._controller: HelloFriendController = None
def configure(self, config):
super().configure(config)
def set_references(self, references):
super(HelloFriendRestService, self).set_references(references)
self._controller = references.get_one_required(Descriptor('hello-friend', 'controller', '*', '*', '1.0'))
def register(self):
self.register_route(method="GET", route="/greeting", schema=Schema(), handler=self.greeting)
self.register_route(method="GET", route="/greeting_create", schema=Schema(), handler=self.create)
def greeting(self):
result = self._controller.greeting()
return self.send_result(result)
def create(self):
correlation_id = self._get_correlation_id()
item = MyFriend(
bottle.request.query["id"],
bottle.request.query["type"],
bottle.request.query["name"]
)
result = self._controller.create(correlation_id, item)
return self.send_result(result)
# Tier 2 : Controller
from pip_services3_commons.config import IConfigurable
from pip_services3_commons.refer import IReferences, IReferenceable
class HelloFriendController(IConfigurable, IReferenceable):
__defaultName = None
__persistence: 'HelloFriendPersistence' = None
def __init__(self):
self.__defaultName = "Pip User"
def configure(self, config):
self.__defaultName = config.get_as_string_with_default("default_name", self.__defaultName)
def set_references(self, references: IReferences):
self.__persistence = references.get_one_required(Descriptor("hello-friend", "persistence", "*", "*", "1.0"))
def greeting(self):
filter_param = FilterParams.from_tuples("type", "friend")
selected_friend = self.__persistence.get_one_random(None, filter_param)
name2 = selected_friend.name
return f"Hello, {name2} !"
def create(self, correlation_id: Optional[str], item: MyFriend) -> MyFriend:
res = self.__persistence.create(correlation_id, item)
return res
# Tier 3 = Persistence
from pip_services3_mysql.persistence import IdentifiableMySqlPersistence
class HelloFriendPersistence(IdentifiableMySqlPersistence):
def __init__(self):
super(HelloFriendPersistence, self).__init__('myfriends3')
def _define_schema(self):
self._clear_schema()
self._ensure_schema(
'CREATE TABLE IF NOT EXISTS `' + self._table_name + '` (id VARCHAR(32) PRIMARY KEY, `type` VARCHAR(50), `name` TEXT)')
def _compose_filter(self, filter: FilterParams):
filter = filter or FilterParams()
type = filter.get_as_nullable_string('type')
name = filter.get_as_nullable_string('name')
filter_condition = ''
if type is not None:
filter_condition += "`type`='" + type + "'"
if name is not None:
filter_condition += "`name`='" + name + "'"
return filter_condition
def get_one_random(self, correlation_id: str, filter: FilterParams) -> MyFriend:
return super().get_one_random(correlation_id, self._compose_filter(filter))
from pip_services3_commons.refer import Descriptor
from pip_services3_components.build import Factory
# Inversion of control: Factory
class HelloFriendServiceFactory(Factory):
def __init__(self):
super(HelloFriendServiceFactory, self).__init__()
HttpServiceDescriptor = Descriptor('hello-friend', 'service', 'http', '*', '1.0') # View
ControllerDescriptor = Descriptor('hello-friend', 'controller', 'default', '*', '1.0') # Controller
PersistenceDescriptor = Descriptor('hello-friend', 'persistence', 'mysql', '*', '1.0') # Persistence
self.register_as_type(HttpServiceDescriptor, HelloFriendRestService) # View
self.register_as_type(ControllerDescriptor, HelloFriendController) # Controller
self.register_as_type(PersistenceDescriptor, HelloFriendPersistence) # Persistence
# Containerization
from pip_services3_container.ProcessContainer import ProcessContainer
from pip_services3_rpc.build import DefaultRpcFactory
class HelloFriendProcess(ProcessContainer):
def __init__(self):
super(HelloFriendProcess, self).__init__('hello-friend', 'HelloFriend microservice')
self._config_path = './config.yaml'
self._factories.add(HelloFriendServiceFactory())
self._factories.add(DefaultRpcFactory())
# Running the app
if __name__ == '__main__':
runner = HelloFriendProcess()
print("run")
try:
runner.run()
except Exception as ex:
print(ex)
Wrapping up
In this tutorial, we have learned how to create a simple application based on a three-tier architecture. First, we saw how to create a view based on a REST service. Then, we understood how to create a controller that manages the connection between the view and the third layer, namely persistence. Next, we saw how to create a persistence layer that includes a MySQL database. Finally, we executed the application and saw the result on our browser.