Object-Oriented Programming (Java/Python)

Chapter 14
Frameworks and Object-Oriented Application Architecture

Bachelors Program in Full Stack Development
Object-Oriented Programming (Java/Python)   •   Bachelor

Frameworks and Object-Oriented Application Architecture

Introduction and Context

Object-oriented programming (OOP) provides a way to organize software around objects that combine state and behavior. Frameworks extend this idea by supplying reusable structures, conventions, libraries, and runtime services for building complete applications. A framework does not merely provide utility functions; it establishes an architectural style and controls significant parts of application execution. This relationship is often described as inversion of control: instead of an application calling a library whenever necessary, the framework calls application-defined components at appropriate points in its lifecycle.

Modern Java and Python applications commonly use frameworks such as Spring Boot, Jakarta EE, Django, Flask, FastAPI, SQLAlchemy, and messaging platforms. Their value is strategic as well as technical. A framework can reduce development time, standardize team practices, improve security, and simplify deployment. However, it can also introduce coupling, hidden behavior, configuration complexity, and upgrade risks. Professional development therefore requires understanding architectural principles rather than memorizing framework APIs.

This chapter examines layered architecture, Model–View–Controller (MVC), service-oriented design, event-driven systems, dependency injection, domain modeling, packaging, documentation, deployment, and maintainable team workflows.

Learning Objectives

By the end of this chapter, you should be able to:

  • explain how frameworks support object-oriented application architecture;
  • distinguish layered, MVC, service-oriented, and event-driven designs;
  • apply dependency injection and inversion of control in Java and Python;
  • model a domain using entities, value objects, services, repositories, and aggregates;
  • organize, package, document, test, and deploy a framework-based application;
  • evaluate architectural trade-offs involving coupling, cohesion, scalability, and maintainability;
  • collaborate using conventions, version control, automated quality checks, and clear interfaces.

1. From Classes to Application Architecture

A class is a local design unit, whereas architecture describes the major units of an entire system and their relationships. A well-designed application assigns responsibilities so that changes in one concern do not unnecessarily affect others.

Three related quality properties are especially important:

  • Cohesion: how closely related the responsibilities within a module are. High cohesion is desirable.
  • Coupling: the degree of dependence between modules. Low coupling is generally desirable.
  • Separation of concerns: the division of presentation, business rules, persistence, integration, and infrastructure into distinguishable responsibilities.

A framework usually provides lifecycle management, routing, configuration, validation, database integration, security support, serialization, logging, and testing facilities. The developer supplies domain-specific behavior and configuration.

A useful architectural dependency rule is that stable business rules should not depend directly on volatile technical details. For example, a pricing rule should not require a particular web server, database driver, or user-interface library. Interfaces can separate the business policy from those details.

Layered application architecture Presentation layer HTTP controllers, views, serializers Application layer Use cases, orchestration, transactions Domain layer Entities, value objects, business rules Infrastructure layer Databases, queues, email, external APIs
Layered application architecture showing presentation, application, domain, and infrastructure layers
Figure: Layered application architecture showing presentation, application, domain, and infrastructure layers

Figure: A conventional layered architecture. Requests generally move downward, while dependencies should be controlled through abstractions.

2. Layered and MVC Architectures

Layered architecture

A common four-layer arrangement is:

  1. Presentation receives input and returns a response.
  2. Application coordinates a use case, such as placing an order.
  3. Domain expresses business concepts and invariants.
  4. Infrastructure communicates with databases, files, queues, and external services.

The layers are logical responsibilities, not necessarily separate processes. A small application may implement them as packages in one deployable unit. A larger system may eventually distribute some responsibilities into services, but distribution should not be mistaken for improved design.

A request to create an order might follow this path:

HTTP request → OrderController → CreateOrderService → Order aggregate → OrderRepository → database

The controller should parse and validate transport data, but it should not calculate discounts or construct SQL statements. The application service coordinates the use case; the domain object protects business rules; the repository hides persistence details.

MVC

MVC separates three concerns:

  • Model represents data and domain behavior.
  • View presents information to a user or client.
  • Controller interprets input and selects application behavior or a view.

In server-side web frameworks, a controller often receives an HTTP request, invokes a service, and returns a template or serialized response. In REST APIs, the “view” may be a JSON representation rather than an HTML page.

MVC is useful, but its name does not guarantee good separation. A “fat controller” that contains validation, business rules, persistence, and formatting is technically part of an MVC application but architecturally weak. The controller should remain a boundary adapter.

3. Service-Oriented and Event-Driven Design

Service-oriented design

A service exposes a coherent capability through an interface. In a modular monolith, services may be ordinary objects within one process. In a distributed system, they may communicate over HTTP, gRPC, or messaging.

A service boundary should reflect a meaningful business capability, such as Catalog, Ordering, or Payments. It should own its rules and data as far as practical. Splitting every class into a network service creates excessive latency, operational burden, and failure modes.

A service contract should specify operations, input and output schemas, error behavior, authentication requirements, and compatibility expectations. OpenAPI can document HTTP interfaces; Java interfaces, Python type hints, and contract tests can improve local clarity.

Event-driven design

An event is a record that something significant has happened, such as OrderPlaced or PaymentAuthorized. An event producer does not need to know every consumer. Consumers subscribe and react asynchronously.

Events support loose coupling and independent scaling, but introduce eventual consistency, ordering concerns, duplicate delivery, retries, and more difficult debugging. Consumers should commonly be idempotent, meaning that processing the same event more than once produces the same intended business result as processing it once. An event identifier and a consumer-side record of processed identifiers are practical techniques.

An event is not the same as a command. A command requests an action, such as AuthorizePayment; an event reports a completed fact, such as PaymentAuthorized.

4. Dependency Injection and Inversion of Control

Dependency injection (DI) means that an object receives the collaborators it needs instead of constructing them internally. This promotes testability and reduces concrete coupling.

Poor design:

class OrderService {
    private final MySqlOrderRepository repository = new MySqlOrderRepository();
}

The service is now tied to MySQL and is difficult to test. Constructor injection is clearer:

class OrderService {
    private final OrderRepository repository;

    OrderService(OrderRepository repository) {
        this.repository = repository;
    }

    Order place(Order order) {
        // domain validation and orchestration
        return repository.save(order);
    }
}

The equivalent Python design can use protocols and constructor injection:

from typing import Protocol

class OrderRepository(Protocol):
    def save(self, order: "Order") -> "Order": ...

class OrderService:
    def __init__(self, repository: OrderRepository):
        self.repository = repository

    def place(self, order: "Order") -> "Order":
        order.confirm()
        return self.repository.save(order)

A framework container constructs objects, resolves dependencies, and manages their lifecycle. Spring uses annotations or configuration to register beans; Jakarta CDI provides contextual injection; FastAPI resolves dependencies declared in endpoint signatures; Django commonly uses explicit composition, although third-party containers are available.

DI design principles

  • Prefer constructor injection for required dependencies.
  • Depend on stable abstractions where substitution is useful.
  • Keep a composition root—the place where concrete implementations are selected.
  • Avoid injecting a service locator and then resolving arbitrary dependencies inside business code.
  • Understand lifetimes: singleton, request-scoped, and transient objects must not be mixed carelessly.
  • Do not make every class an injectable component without a reason.

DI is not automatically beneficial. An interface with only one implementation and no expected variation may add unnecessary indirection. The design should reflect actual volatility and testing needs.

5. Domain Modeling with Objects

Domain modeling translates business concepts into software concepts. A robust model usually distinguishes the following.

ConceptPurposeExample
EntityHas identity over timeCustomer, Order
Value objectDefined by attributes, usually immutableMoney, Address
AggregateConsistency boundary with a root entityOrder with OrderLines
RepositoryCollection-like persistence abstractionOrderRepository
Domain serviceDomain operation not naturally owned by one entityTaxCalculator
Application serviceCoordinates a use casePlaceOrderService
Domain eventRecords a meaningful domain factOrderPlaced

An entity’s identity distinguishes it even if other attributes change. A value object such as Money is defined by amount and currency and should normally be immutable. A domain invariant is a condition that must remain true, such as “an order cannot be shipped before payment is authorized.”

An aggregate should be small enough to maintain consistency without excessive locking or transaction cost. External code should normally modify it through its aggregate root rather than changing internal members directly.

Example:

from dataclasses import dataclass
from decimal import Decimal

@dataclass(frozen=True)
class Money:
    amount: Decimal
    currency: str

class Order:
    def __init__(self, order_id: str):
        self.id = order_id
        self.status = "draft"
        self.lines = []

    def add_line(self, product_id: str, quantity: int, price: Money):
        if self.status != "draft":
            raise ValueError("Only draft orders can change")
        if quantity <= 0:
            raise ValueError("Quantity must be positive")
        self.lines.append((product_id, quantity, price))

    def confirm(self):
        if not self.lines:
            raise ValueError("An order requires at least one line")
        self.status = "confirmed"

The example illustrates encapsulation: callers cannot legitimately confirm an empty order by directly setting arbitrary state through a public business operation. In production code, richer line objects, explicit error types, and persistence mapping would usually be appropriate.

6. Framework Implementation Patterns

Java and Spring Boot

A typical Spring Boot application contains controllers, services, repositories, entities or persistence models, configuration, and tests. A simplified arrangement is:

@RestController
class OrderController {
    private final OrderService service;

    OrderController(OrderService service) {
        this.service = service;
    }

    @PostMapping("/orders")
    ResponseEntity<OrderResponse> create(@RequestBody CreateOrderRequest request) {
        Order order = service.create(request);
        return ResponseEntity.status(201).body(OrderResponse.from(order));
    }
}

Spring can inject the OrderService through its application context. Validation annotations, transaction boundaries, exception handlers, and configuration should be used deliberately. Persistence entities should not automatically become API response models: exposing internal structure creates an unstable public contract and can leak sensitive fields.

Python and Django or FastAPI

Django provides routing, views, an ORM, templates, forms, authentication, and an administrative interface. FastAPI emphasizes typed request handling, dependency resolution, asynchronous endpoints, and automatic OpenAPI documentation. Neither framework removes the need for domain design.

A FastAPI boundary might look like:

from fastapi import FastAPI, Depends

app = FastAPI()

def get_order_service() -> OrderService:
    return OrderService(repository=SqlOrderRepository())

@app.post("/orders", status_code=201)
def create_order(request: CreateOrderRequest,
                service: OrderService = Depends(get_order_service)):
    return service.place(request.to_domain())

The dependency function is a composition mechanism. In testing, it can be overridden with an in-memory repository. Asynchronous code should be used when the whole path benefits from non-blocking I/O; simply declaring a function asynchronous does not make CPU-bound work faster.

7. Packaging, Documentation, Testing, and Deployment

Packaging

A maintainable package structure should communicate boundaries. One possible Python structure is:

src/shop/
  domain/
  application/
  infrastructure/
  presentation/
tests/
pyproject.toml
README.md

A Java project commonly uses Maven or Gradle with packages such as com.example.shop.domain, application, and infrastructure. Package names should follow a stable convention, and public APIs should be intentionally small.

Dependency versions should be reproducible through lock files or dependency management. Transitive dependencies need review because they affect security, licensing, and upgrade behavior.

Documentation

A professional project should document:

  • purpose, scope, and local setup;
  • architecture and significant decisions;
  • API contracts and example requests;
  • configuration variables and secrets management;
  • database migration and rollback procedures;
  • testing and release commands;
  • operational contacts, metrics, and failure recovery.

Architecture decision records (ADRs) capture the context, decision, alternatives, and consequences of important choices. Documentation should be maintained as part of the code-change workflow rather than written only at project completion.

Testing strategy

Unit tests target domain objects and application services with fast substitutes. Integration tests verify database, framework, and messaging boundaries. End-to-end tests verify a limited number of critical user journeys. Contract tests check that service providers and consumers agree on schemas and behavior.

A large collection of end-to-end tests may be slow and fragile, whereas only unit tests may miss configuration and integration defects. A balanced test portfolio is more useful than a simplistic numerical target.

Deployment

A typical deployment pipeline performs formatting, static analysis, unit tests, integration tests, security scanning, packaging, and release automation. Applications may be deployed as executable JAR files, Python wheels and containers, or platform-specific artifacts.

Configuration should be externalized. Secrets should not be committed to source control. Health checks should distinguish liveness (the process is running) from readiness (the process can serve traffic). Safe database migration practice considers backward compatibility, backup, rollback, and concurrent versions during deployment.

Observability combines logs, metrics, and traces. Correlation identifiers allow a request to be followed across services, but logs must avoid passwords, tokens, unnecessary personal data, and payment details.

8. Illustrative Case: Order Processing Platform

The following is a fictional illustrative case.

Background

A growing retailer has a Python web application that handles browsing, orders, payments, and notifications in one large module. The application has duplicated discount logic, slow tests, and frequent failures when an external payment provider is unavailable.

Problem analysis

The HTTP views directly access the database and payment SDK. A single request performs order validation, payment authorization, inventory updates, and email sending. The design has high coupling and no clear transaction or retry boundaries. An email failure can cause a customer to believe that the order failed even after payment succeeded.

Approach

The team introduces:

  1. a domain Order aggregate enforcing order invariants;
  2. an application service for the PlaceOrder use case;
  3. repository and payment-provider interfaces;
  4. a database adapter and a payment adapter in infrastructure;
  5. an API controller that maps HTTP data to commands;
  6. an OrderPlaced event published after the order transaction;
  7. a notification consumer that sends email independently and retries safely;
  8. contract tests for the payment adapter and API schema.

A transactional outbox records the event in the same database transaction as the order. A separate publisher transfers outbox records to the message broker. This reduces the risk that the order is committed while the event is lost, although it requires monitoring and cleanup of the outbox.

Outcome

Unit tests can exercise pricing and order rules without a database. Payment failures are represented explicitly and retried according to provider guidance. Email delays no longer roll back a completed order. The team can change the payment provider by implementing the payment interface and its contract tests.

The redesign does not eliminate all difficulty. Distributed processing can deliver duplicate events, so notification handling uses an event identifier and an idempotency record. Operational dashboards track failed messages and outbox age.

Learning points

Architecture should follow business boundaries, not merely technical layers. Asynchronous processing improves isolation but introduces eventual consistency. Interfaces, contracts, and observability are as important as class design. A modular monolith may be the best intermediate architecture before considering independently deployed services.

9. Team Workflows and Professional Practice

Maintainability is a team property. Teams should agree on naming, package boundaries, formatting, branching, commit quality, review expectations, and definition of done. Small pull requests are easier to review. Code review should examine correctness, security, domain fit, test adequacy, and operational consequences rather than personal style preferences already enforced by tools.

Continuous integration should run automatically for every change. Trunk-based development or short-lived branches reduce merge divergence. Pairing and design discussions distribute architectural knowledge and reduce dependence on one specialist.

Security must be designed into framework applications. Validate input at boundaries, authorize actions based on identity and policy, use parameterized database operations, protect against cross-site request forgery where applicable, configure secure headers, and update dependencies. Privacy principles require collecting only necessary personal data, restricting access, defining retention, and providing appropriate deletion or export mechanisms under applicable law.

AI-assisted coding can accelerate scaffolding and test generation, but generated code requires human review. Teams should not paste confidential source code or personal data into unapproved tools. AI output may contain insecure dependencies, licensing concerns, hallucinated APIs, or subtle domain errors. The responsible developer remains accountable for verification, testing, and compliance.

10. Architectural Trade-offs and Limitations

No architecture is universally superior.

ChoiceStrengthCost or risk
Layered monolithSimple deployment and debuggingCan become tightly coupled
Modular monolithStrong boundaries without network costRequires discipline to preserve modules
MicroservicesIndependent deployment and scalingOperational and distributed-systems complexity
Synchronous callsSimple control flow and immediate responseCoupling and cascading failures
Asynchronous eventsLoose coupling and resilienceEventual consistency and harder tracing
Framework-heavy designProductivity and standard facilitiesHidden behavior and upgrade dependency
Minimal framework useExplicit control and small runtimeMore infrastructure code to build and maintain

Architecture should be evaluated against team skill, delivery risk, regulatory requirements, expected load, change frequency, and operational capability. Performance claims should be measured under representative workloads rather than inferred from fashionable patterns.

Contemporary Directions

Current framework development increasingly emphasizes typed interfaces, automated API schemas, containerized deployment, cloud-native observability, and platform engineering. Java applications commonly use records, modules where appropriate, and reactive or virtual-thread options according to workload characteristics. Python teams increasingly use type checkers, asynchronous web frameworks, and dependency management through standardized project metadata.

These developments do not replace OOP fundamentals. Encapsulation, explicit contracts, substitutable dependencies, domain-centered design, and controlled complexity remain applicable whether code runs in a monolith, container, function platform, or event-processing system.

Chapter Summary

Frameworks provide lifecycle management, routing, validation, persistence integration, configuration, and other reusable capabilities, but they do not automatically produce good architecture. Layered architecture separates presentation, application, domain, and infrastructure concerns. MVC organizes interaction among models, controllers, and views. Service-oriented design exposes business capabilities through explicit contracts, while event-driven design communicates completed facts asynchronously and must address retries, ordering, and idempotency.

Dependency injection reduces concrete coupling and supports testing by allowing collaborators to be supplied from a composition root. Domain modeling distinguishes entities, value objects, aggregates, repositories, domain services, application services, and domain events. Professional applications also require reproducible packaging, useful documentation, balanced testing, secure configuration, observability, automated deployment, and disciplined team workflows. Architectural choices involve trade-offs; modularity and simplicity should be preferred over unnecessary distribution. Frameworks and AI tools are valuable when used critically, securely, and with human accountability.

Key Terms

Aggregate: A consistency boundary containing a root entity and related objects.

Application service: A component that coordinates a particular use case.

Cohesion: The degree to which responsibilities within a module belong together.

Composition root: The location where concrete implementations and dependencies are assembled.

Controller: A boundary component that interprets input and invokes application behavior.

Dependency injection: Supplying an object’s collaborators from outside rather than constructing them internally.

Domain event: A record that a significant business fact has occurred.

Entity: A domain object identified by continuity of identity.

Eventual consistency: A state in which distributed components may temporarily differ but converge after processing.

Framework: A reusable application structure that controls lifecycle and invokes developer-defined components.

Inversion of control: Transfer of execution or object-creation control from application code to a framework or container.

Idempotency: The property that repeated processing has the same intended effect as one processing.

MVC: Model–View–Controller separation of data/domain representation, presentation, and input coordination.

Repository: An abstraction for obtaining and storing domain objects.

Service contract: An agreed interface describing operations, data, errors, and compatibility behavior.

Value object: An usually immutable object defined by its attributes rather than identity.

Review Questions

Short Answer Questions

  1. What is inversion of control, and how does it differ from ordinary library use?
  2. Distinguish an entity from a value object.
  3. Why is constructor injection generally preferable for required dependencies?
  4. What is the difference between an event and a command?
  5. State two reasons why a controller should not contain all business logic.

Long Answer Questions

  1. Explain layered architecture and discuss the responsibilities of its presentation, application, domain, and infrastructure layers.
  2. Compare MVC and service-oriented design. Explain how they can coexist in one web application.
  3. Discuss the benefits and risks of event-driven architecture, including retries, duplicate delivery, and eventual consistency.
  4. Describe a professional packaging, testing, documentation, and deployment workflow for a Java or Python application.
  5. Explain how domain modeling and dependency injection jointly improve maintainability and testability.

Analytical / Application Questions

  1. A payment provider is slow and sometimes unavailable. Design a framework-based order workflow that protects the domain model, handles failure, and prevents duplicate payment processing.
  2. A team proposes splitting a small modular monolith into twelve microservices immediately. Analyze the proposal using coupling, deployment, operational complexity, team capability, and business boundaries.
  3. Given a controller that validates orders, calculates discounts, writes SQL, calls an email API, and returns JSON, identify its architectural problems and propose a refactoring plan.

Multiple Choice Questions with Answers

Question 1

Which statement best describes dependency injection?

A. A class constructs every collaborator internally

B. A framework automatically encrypts all object state

C. An object receives its collaborators from outside

D. A database creates objects from table rows

Correct Answer: C. An object receives its collaborators from outside

Question 2

Which layer should primarily contain business invariants such as the rule that an empty order cannot be confirmed?

A. Presentation layer

B. Domain layer

C. Infrastructure layer

D. Deployment layer

Correct Answer: B. Domain layer

Question 3

What does an event usually communicate?

A. A request that a consumer must perform an action

B. A completed or observed fact

C. A database connection string

D. A user-interface template

Correct Answer: B. A completed or observed fact

Question 4

Which practice most directly helps an event consumer tolerate duplicate delivery?

A. Idempotent processing

B. Using longer class names

C. Removing event identifiers

D. Putting all consumers in one controller

Correct Answer: A. Idempotent processing

Question 5

What is the primary purpose of a repository abstraction?

A. To render HTML pages

B. To hide persistence details from domain or application code

C. To replace all domain objects

D. To configure a network firewall

Correct Answer: B. To hide persistence details from domain or application code

Question 6

Which choice is generally most appropriate for a required dependency?

A. Constructor injection

B. A global mutable variable

C. Creating it inside every method

D. Reading it from arbitrary environment state throughout the domain

Correct Answer: A. Constructor injection

Question 7

Which statement about a modular monolith is correct?

A. It must contain independently deployed services

B. It has logical module boundaries while remaining one deployable unit

C. It cannot use dependency injection

D. It requires asynchronous messaging for every call

Correct Answer: B. It has logical module boundaries while remaining one deployable unit

Question 8

What is the main architectural risk of a fat controller?

A. It makes HTTP impossible

B. It combines boundary, business, persistence, and formatting responsibilities

C. It prevents all unit testing by definition

D. It guarantees excessive database normalization

Correct Answer: B. It combines boundary, business, persistence, and formatting responsibilities

Question 9

Which document is most suitable for recording the reasoning and consequences of an important architecture choice?

A. Architecture decision record

B. Binary lock file

C. CSS stylesheet

D. Database index

Correct Answer: A. Architecture decision record

Question 10

Which statement about asynchronous event-driven systems is accurate?

A. They eliminate all failure handling

B. They always provide immediate consistency

C. They can reduce coupling but introduce concerns such as retries and ordering

D. They require every component to share one database transaction

Correct Answer: C. They can reduce coupling but introduce concerns such as retries and ordering

Object-Oriented Programming (Java/Python)
Chapter 14 of 15