Lets assume the following layered architecture of our application.
Our Application has two entry points, the command line and the http interface (either a request for a html document or for json data). Furthermore they are both using the same business logic. The Business Logic has access to the Database and the Filesystem to store and retrieve data or files.
The Physical Infrastructure is an representation of the database and the filesystem. It is not part of the application, but it is needed to run the application. Depending on the environment the application is running in, the physical infrastructure can be different. For example in a test environment the database can be an in memory database, while in production it is a real database.
We assume that we are using MySQL and an S3-compatible Filesystem in the Production environment and during Development we are using sqlite and the local filesystem.
The layers are communicating with each other via contracts. A contract is an interface, which defines the methods that can be called. The contract is implemented by the layer that is using it. The layer that is using the contract is called the consumer and the layer that is implementing the contract is called the provider.
Lets assume we are developing a simple blog where you can create, edit and delete posts and also manage the users of the blog.
For that we need functionality the ability to list, retrieve, store, update and delete posts and users to/from the database. This could be a simple contract for the Database Componenent:
Database/UserRepository
- all(): User[]
- find(id: int): User
- store(user: User): void
- update(user: User): void
- delete(user: User): void
Database/User
- id: int
- name: string
- email: string
- password: string (hashed)
- createdAt: DateTime
- updatedAt: DateTime
Database/PostRepository
- all(): Post[]
- find(id: int): Post
- store(post: Post): void
- update(post: Post): void
- delete(post: Post): void
Database/Post
- id: int
- title: string
- content: string
- files: File[]
- createdAt: DateTime
- updatedAt: DateTime
The Database Component is the provider of the contract and the Business Logic Component is the consumer of the contract.
For the filesystem we want the ability to retrieve, store and delete files, which can be used in blog posts (for example images or pdf files for download). This could be a simple contract for the Filesystem Component:
Filesystem/FileRepository
- get(path: string): File
- store(path: string, file: File): void
- delete(path: string): void
Filesystem/File
- path: string
- content: string
The Filesystem Component is the provider of the contract and the Business Logic Component is the consumer of the contract.
The Business Logic Component is the consumer of the Database and Filesystem Component. It is also the provider of the contract for the Command Line and Http Interface.
We assume that our Business Logic has single responsibility classes for each use case. For example a class for creating a post, a class for editing a post and a class for deleting a post. The same applies for the users.
BusinessLogic/Users/ListUsers
- canBeExecutedBy(user: User): bool
- execute(): User[]
BusinessLogic/Users/ShowUser
- canBeExecutedBy(user: User): bool
- execute(id: int): User
BusinessLogic/Users/CreateUser
- canBeExecutedBy(user: User): bool
- validate(name: string, email: string, password: string): bool
- execute(name: string, email: string, password: string): User
BusinessLogic/Users/EditUser
- canBeExecutedBy(user: User): bool
- validate(user: User, name: string, email: string, password: string): bool
- execute(user: User, name: string, email: string, password: string): User
BusinessLogic/Users/DeleteUser
- canBeExecutedBy(user: User): bool
- validate(user: User): bool
- execute(user: User): void
BusinessLogic/Posts/ListPosts
- canBeExecutedBy(user: User): bool
- execute(): Post[]
BusinessLogic/Posts/ShowPost
- canBeExecutedBy(user: User): bool
- execute(id: int): Post
BusinessLogic/Posts/CreatePost
- canBeExecutedBy(user: User): bool
- validate(title: string, content: string, files: File[]): bool
- execute(title: string, content: string, files: File[]): Post
BusinessLogic/Posts/EditPost
- canBeExecutedBy(user: User): bool
- validate(post: Post, title: string, content: string, files: File[]): bool
- execute(post: Post, title: string, content: string, files: File[]): Post
BusinessLogic/Posts/DeletePost
- canBeExecutedBy(user: User): bool
- validate(post: Post): bool
- execute(post: Post): void
The Console Component is a consumer of the Business Logic Component.
Here you can only create, update or delete users, but not posts. This could be a simple contract for the Console Component:
Console/Users/CreateUserCommand
- execute(args: string[]): void
Console/Users/EditUserCommand
- execute(args: string[]): void
Console/Users/DeleteUserCommand
- execute(args: string[]): void
We assume these commands can be called as follows:
app/console user:create <name> <email> <password>
app/console user:edit <id> <name> <email> <password>
app/console user:delete <id>
The Http Component is a consumer of the Business Logic Component.
We have two interfaces available where requests can be coming to. One is for the browser, where we want to return a html document and the other one is for a mobile app, where we want to return json data.
Depending on where the request is coming from we have different authentication methods. For example if the request is coming from the browser, we can use a session cookie to authenticate the user. If the request is coming from a mobile app, we can use a token to authenticate the user.
This could be a simple contract for the Http Component:
Http/Users/ListUsersController
- viewList(request: WebRequest): HtmlResponse
- listApi(request: ApiRequest): JsonResponse
Http/Users/ShowUserController
- viewShow(request: WebRequest): HtmlResponse
- showApi(request: ApiRequest): JsonResponse
Http/Users/CreateUserController
- viewForm(request: WebRequest): HtmlResponse
- storeWeb(request: WebRequest): HtmlResponse
- storeApi(request: ApiRequest): JsonResponse
Http/Users/EditUserController
- viewForm(request: WebRequest): HtmlResponse
- updateWeb(request: WebRequest): HtmlResponse
- updateApi(request: ApiRequest): JsonResponse
Http/Users/DeleteUserController
- viewForm(request: WebRequest): HtmlResponse
- deleteWeb(request: WebRequest): HtmlResponse
- deleteApi(request: ApiRequest): JsonResponse
Http/Posts/ListPostsController
- viewList(request: WebRequest): HtmlResponse
- listApi(request: ApiRequest): JsonResponse
Http/Posts/ShowPostController
- viewShow(request: WebRequest): HtmlResponse
- showApi(request: ApiRequest): JsonResponse
Http/Posts/CreatePostController
- viewForm(request: WebRequest): HtmlResponse
- storeWeb(request: WebRequest): HtmlResponse
- storeApi(request: ApiRequest): JsonResponse
Http/Posts/EditPostController
- viewForm(request: WebRequest): HtmlResponse
- updateWeb(request: WebRequest): HtmlResponse
- updateApi(request: ApiRequest): JsonResponse
Http/Posts/DeletePostController
- viewForm(request: WebRequest): HtmlResponse
- deleteWeb(request: WebRequest): HtmlResponse
- deleteApi(request: JsonRequest): JsonResponse
Now we only need to map the request to the right controller and the right method. For example:
// Web Request
| Method | Path | Controller::Method |
| ------ | ---------------- | ----------------------------------------- |
| GET | /users | Http/Users/ListUsersController::viewList |
| GET | /users/create | Http/Users/CreateUserController::viewForm |
| GET | /users/{id} | Http/Users/ShowUserController::viewShow |
| GET | /users/{id}/edit | Http/Users/EditUserController::viewForm |
| POST | /users | Http/Users/CreateUserController::storeWeb |
| PUT | /users/{id} | Http/Users/EditUserController::updateWeb |
| DELETE | /users/{id} | Http/Users/EditUserController::updateWeb |
// API Request
| Method | Path | Controller::Method |
| ------ | ---------------- | ----------------------------------------- |
| GET | /api/users | Http/Users/ListUsersController::listApi |
| GET | /api/users/{id} | Http/Users/ShowUserController::showApi |
| POST | /api/users | Http/Users/CreateUserController::storeApi |
| PUT | /api/users/{id} | Http/Users/EditUserController::updateApi |
| DELETE | /api/users/{id} | Http/Users/EditUserController::updateApi |
// Web Request
| Method | Path | Controller::Method |
| ------ | ---------------- | ----------------------------------------- |
| GET | /posts | Http/Posts/ListPostsController::viewList |
| GET | /posts/create | Http/Posts/CreatePostController::viewForm |
| GET | /posts/{id} | Http/Posts/ShowPostController::viewShow |
| GET | /posts/{id}/edit | Http/Posts/EditPostController::viewForm |
| POST | /posts | Http/Posts/CreatePostController::storeWeb |
| PUT | /posts/{id} | Http/Posts/EditPostController::updateWeb |
| DELETE | /posts/{id} | Http/Posts/EditPostController::updateWeb |
// API Request
| Method | Path | Controller::Method |
| ------ | ---------------- | ----------------------------------------- |
| GET | /api/posts | Http/Posts/ListPostsController::listApi |
| GET | /api/posts/{id} | Http/Posts/ShowPostController::showApi |
| POST | /api/posts | Http/Posts/CreatePostController::storeApi |
| PUT | /api/posts/{id} | Http/Posts/EditPostController::updateApi |
| DELETE | /api/posts/{id} | Http/Posts/EditPostController::updateApi |
Now you can start implementing the contracts according to the definition.
How could for example the implementation of the Http/Users/ListUsersController
look like?
use BusinessLogic/Users/ListUsers as ListUsers
viewList(request: WebRequest): HtmlResponse
{
// returns the user from the session cookie
user = request->getUser()
if user is null
then return HtmlResponse(
status: 401,
content: "Unauthenticated"
)
if not ListUsers::canBeExecutedBy(user)
then return HtmlResponse(
status: 403,
content: "Forbidden"
)
users = ListUsers::execute()
return HtmlResponse(
status: 200,
content: render("users/list", users: users)
)
}
use BusinessLogic/Users/ListUsers as ListUsers
listApi(request: ApiRequest): JsonResponse
{
// returns the user from the Bearer token
user = request->getUser()
if user is null
then return JsonResponse(
status: 401,
content: "Unauthenticated"
)
if not ListUsers::canBeExecutedBy(user)
then return JsonResponse(
status: 403,
content: "Forbidden"
)
users = ListUsers::execute()
return JsonResponse(
status: 200,
data: users
)
}
How could the implementation of the Http/Users/CreateUserController
look like?
use BusinessLogic/Users/CreateUser as CreateUser
viewForm(request: WebRequest): HtmlResponse
{
// returns the user from the session cookie
user = request->getUser()
if user is null
then return HtmlResponse(
status: 401,
content: "Unauthenticated"
)
if not CreateUser::canBeExecutedBy(user)
then return HtmlResponse(
status: 403,
content: "Forbidden"
)
return HtmlResponse(
status: 200,
content: render("users/create")
)
}
use BusinessLogic/Users/CreateUser as CreateUser
storeWeb(request: WebRequest): HtmlResponse
{
// returns the user from the session cookie
user = request->getUser()
if user is null
then return HtmlResponse(
status: 401,
content: "Unauthenticated"
)
if not CreateUser::canBeExecutedBy(user)
then return HtmlResponse(
status: 403,
content: "Forbidden"
)
name = request->get("name")
email = request->get("email")
password = request->get("password")
if not CreateUser::validate(name, email, password)
then return HtmlResponse(
status: 400,
content: "Bad Request"
)
user = CreateUser::execute(name, email, password)
return HtmlResponse(
status: 302,
content: redirect("users/{user.id}")
)
}
use BusinessLogic/Users/CreateUser as CreateUser
storeApi(request: ApiRequest): JsonResponse
{
// returns the user from the Bearer token
user = request->getUser()
if user is null
then return JsonResponse(
status: 401,
content: "Unauthenticated"
)
if not CreateUser::canBeExecutedBy(user)
then return JsonResponse(
status: 403,
content: "Forbidden"
)
name = request->get("name")
email = request->get("email")
password = request->get("password")
if not CreateUser::validate(name, email, password)
then return JsonResponse(
status: 400,
content: "Bad Request"
)
user = CreateUser::execute(name, email, password)
return JsonResponse(
status: 201,
data: user
)
}
In our example you always have the same steps you follow when implementing a new feature:
This makes it easy to add new features to the application and also to find the right place to implement the feature.
In our Http Component all the controllers follow the same pattern:
This makes it easy to understand how the Http Component works and also makes it easy to add new controllers.
This can be applied to other applications, were you have different entry points into the system. For example a command line interface and a http interface. Or a command line interface and a message queue. Or a http interface and a message queue. Or a command line interface, a http interface and a message queue.
My Homepage
·Contact
·
Privacy Policy
© 2025 Julius Kiekbusch