Skip to content

Model-View-Controller (MVC)

Model-View-Controller separates presentation into three interconnected components — Model (data + business logic), View (display), and Controller (input handling).

This page shows how ForgingBlocks concepts can be projected onto an MVC arrangement.

Important

ForgingBlocks does not enforce MVC.
This page presents it as an interpretation, not a required structure.


Quick summary

Model-View-Controller (MVC) separates presentation into three interconnected components — Model (data + business logic), View (display), and Controller (input handling). This page shows how ForgingBlocks concepts can be projected onto this arrangement — not enforced.

Mapping:
- Model — Domain (Entities, Value Objects, Aggregates) + Application (Use Cases, business logic)
- View — Presentation (rendering, output formatting)
- Controller — Presentation (input handling, routing, request parsing)
- Ports keep Model decoupled from View and Controller

Fits when: user-facing applications with clear input/display separation; traditional request-response flows; frameworks assume MVC.
Consider alternatives when: model must be fully isolated from presentation; event-driven updates dominate; complex UI synchronization required.


Conceptual mapping

  • The Model holds business logic and state (Domain + Application).
  • The View renders output and displays state.
  • The Controller handles input, routes requests, and parses parameters.
  • ForgingBlocks ports keep the Model decoupled from the View and Controller.

The diagram below shows a canonical MVC view from the literature, independent of ForgingBlocks.

---
title: Model-View-Controller (MVC)
---
graph LR
    Controller[Controller<br/>Input Handling, Routing] -->|updates| Model[Model<br/>Business Logic, State]
    Model -->|notifies| View[View<br/>Display, Output]
    View -->|renders| User[User]
    User -->|interacts| Controller

ForgingBlocks in practice

Model: Domain Entities and Value Objects

The Model layer maps cleanly to ForgingBlocks Domain concepts. Entities represent identity-bearing objects, while Value Objects encapsulate validated, immutable values.

from forging_blocks.domain.entity import Entity
from forging_blocks.domain.value_object import ValueObject


class Email(ValueObject[str]):
    __slots__ = ("_value",)

    def __init__(self, value: str) -> None:
        super().__init__()
        if "@" not in value:
            raise ValueError("Invalid email")
        self._value = value

    @property
    def value(self) -> str:
        return self._value


class User(Entity[int]):
    def __init__(self, user_id: int, email: Email) -> None:
        super().__init__(user_id)
        self._email = email
        self._active = True

    @property
    def email(self) -> Email:
        return self._email

    def deactivate(self) -> None:
        self._active = False

Controller: Application Service Ports

The Controller can be backed by an ApplicationServicePort — an inbound port that defines the contract for use case execution. The controller parses input and delegates to the port.

from abc import abstractmethod
from dataclasses import dataclass
from forging_blocks.application.ports.inbound.application_service_port import (
    ApplicationServicePort,
)


@dataclass(frozen=True)
class CreateUserRequest:
    email: str


@dataclass(frozen=True)
class CreateUserResponse:
    user_id: int
    email: str


class CreateUserPort(
    ApplicationServicePort[CreateUserRequest, CreateUserResponse]
):
    @abstractmethod
    async def execute(self, request: CreateUserRequest) -> CreateUserResponse:
        """Create a new user from the given request."""

Result-returning Controller

A concrete controller delegates to the use case port and translates the outcome for the View using Result.

from forging_blocks.foundation.result import Ok, Err, Result


class UserController:
    def __init__(self, create_user: CreateUserPort) -> None:
        self._create_user = create_user

    async def handle(
        self, request: CreateUserRequest,
    ) -> Result[CreateUserResponse, str]:
        try:
            response = await self._create_user.execute(request)
            return Ok(response)
        except ValueError as exc:
            return Err(str(exc))

When this style fits

  • User-facing applications with clear input/display separation.
  • Traditional request-response flows.
  • Frameworks assume an MVC structure.

When to consider alternatives

  • The Model must be fully isolated from all presentation concerns.
  • Event-driven updates dominate.
  • Complex UI synchronization is required.