Bachelors Program in Full Stack Development
Software Engineering Principles
•
Bachelor
Software Modeling and Design Principles
Introduction
Software systems become difficult to build and maintain when their structure exists only in developers’ minds. Modeling makes structure visible; design principles make decisions consistent; refactoring keeps the structure healthy as requirements change. In full-stack development, these activities connect user needs, domain concepts, application services, interfaces, databases, and deployment environments.
A software model is a purposeful representation of a system. It may describe structure, behavior, data, interactions, or deployment. A model is not the software itself and is never complete in every detail. Its value lies in helping stakeholders reason about selected concerns before implementation becomes expensive.
This chapter develops a practical design vocabulary based on UML, complementary models, decomposition, abstraction, encapsulation, information hiding, separation of concerns, SOLID principles, coupling, cohesion, contracts, refactoring, and recognized design patterns.
Learning Objectives
By the end of this chapter, you should be able to:
Explain the purpose and limits of software models.
Select suitable UML and complementary models for a design problem.
Apply decomposition, abstraction, encapsulation, information hiding, and separation of concerns.
Evaluate coupling and cohesion in a software design.
Apply SOLID principles and design by contract.
Recognize when refactoring or a design pattern is appropriate.
Analyze a full-stack design and justify architectural decisions.
1. Modeling as a Design Activity
1.1 Why models matter
A model supports communication, analysis, planning, and validation. A product owner may use a use-case model to discuss goals, while a developer uses a class or component model to discuss responsibilities. A database designer uses an entity–relationship model to check data constraints, and an operations engineer uses a deployment model to examine runtime dependencies.
Good models are:
Purposeful: they answer a defined question.
Consistent: names and relationships have stable meanings.
Traceable: important elements can be related to requirements or code.
Understandable: unnecessary detail is excluded.
Evolvable: they can be updated as knowledge improves.
A model may be descriptive, showing an existing system, or prescriptive, proposing a system to be built. It may also be conceptual, logical, or physical. For example, “Customer places Order” is conceptual; a class diagram with Customer and Order is logical; database tables and indexes are physical.
1.2 UML and complementary models
The Unified Modeling Language (UML) is a standardized notation for visualizing and specifying software systems. UML is not a development process and does not prescribe a particular architecture. Common UML diagrams include:
Model
Main question answered
Typical use
Use-case diagram
Who wants what from the system?
Scope and stakeholder goals
Class diagram
What structural concepts and relationships exist?
Domain and object design
Sequence diagram
How do participants collaborate over time?
Request flow and responsibility analysis
Activity diagram
What workflow or decision sequence occurs?
Business processes
State-machine diagram
How does an object change state?
Lifecycle-heavy objects
Component diagram
What replaceable software units depend on each other?
Architecture
Deployment diagram
Where do software elements execute?
Infrastructure and operations
Complementary models are often essential. A relational schema describes tables and constraints; an API specification describes endpoints, messages, and error responses; a domain glossary controls vocabulary; a decision record explains why an architectural choice was made; and a data-flow diagram identifies movement and transformation of information.
The appropriate modeling rule is: model enough to reduce uncertainty, but not so much that modeling becomes detached from delivery. A sequence diagram is valuable when responsibility or ordering is unclear; it is less valuable when it merely reproduces obvious code.
UML sequence showing an order checkout flow
Figure: UML sequence showing an order checkout flow
Figure: A sequence model clarifies the order of messages and the responsibilities of participating components.
2. Core Design Principles
2.1 Decomposition
Decomposition divides a complex system into smaller parts with identifiable responsibilities. A full-stack order system might be decomposed into presentation, application, domain, persistence, and external-integration concerns. Decomposition reduces cognitive load and permits independent testing, but excessive fragmentation creates navigation overhead and integration complexity.
A useful decomposition follows stable responsibilities rather than arbitrary technical nouns. “Everything related to orders” may be a better boundary than separate modules for every database table if order rules are distributed across those tables and services.
2.2 Abstraction
Abstraction exposes essential properties while suppressing irrelevant detail. A PaymentGateway abstraction may expose authorize(amount) without exposing HTTP headers, retry policies, or a particular vendor. Abstraction enables substitution and reduces the number of facts a client must understand.
An abstraction should be judged by the decisions it hides. If clients must know vendor-specific status codes or internal database states, the abstraction is leaking implementation detail.
2.3 Encapsulation and information hiding
Encapsulation groups state and behavior behind a boundary. Information hiding is the design decision that particular implementation knowledge should remain private because it is likely to change. These concepts overlap but are not identical: a class can encapsulate data while still exposing unstable representation details.
For example, an Order should protect the rule that an item can be added only while the order is open. Clients call addItem(product, quantity) rather than directly modifying an exposed list. The object maintains its invariant—such as “quantity must be positive”—at the boundary where invalid state could otherwise enter.
2.4 Separation of concerns
A concern is a responsibility or reason for change. Separation of concerns assigns distinct concerns to distinct modules or layers. A controller should translate an HTTP request, an application service should coordinate a use case, a domain object should enforce business rules, and a repository should manage persistence. These responsibilities can collaborate without being merged.
Separation is not absolute isolation. Some cross-cutting concerns, including logging, authentication, and transaction management, affect many modules. They should be handled consistently through middleware, decorators, aspects, or framework mechanisms rather than copied inconsistently throughout business code.
3. Coupling and Cohesion
Coupling describes the degree of dependency between modules. Cohesion describes how strongly the responsibilities within a module belong together. A maintainable design generally seeks lower unnecessary coupling and higher cohesion.
Examples of problematic coupling include global mutable state, direct dependence on a concrete payment vendor, shared database tables used as an informal integration interface, and modules that require knowledge of another module’s internal sequence of operations. Cohesion is weak when a class validates orders, sends email, formats HTML, and calculates tax for unrelated regions.
Coupling and cohesion are design heuristics, not independent numerical laws. A highly cohesive module may still be tightly coupled to a stable domain abstraction, and low coupling can be harmful if it is achieved by scattering a single responsibility across many difficult-to-understand components. Evaluate dependency direction, volatility, testability, and comprehension together.
Useful questions include:
What must change if this requirement changes?
Can this component be tested without a database or network?
Does a module have one clear reason to change?
Are dependencies directed toward stable abstractions?
Does a caller need to know internal ordering or representation?
4. SOLID Principles
SOLID is a set of object-oriented design guidelines. They are not rigid laws; applying them mechanically can produce needless interfaces and indirection.
Single Responsibility Principle
A class or module should have one coherent responsibility and one principal reason to change. A CheckoutService may coordinate checkout, while a tax policy calculates tax. Responsibility is about cohesive change, not simply the number of methods.
Open–Closed Principle
Software entities should be open for extension but closed for modification in contexts where new variants are expected. A shipping calculator can depend on a ShippingPolicy interface, allowing new policies without editing stable checkout orchestration. This principle must be balanced against speculative generalization; abstractions should be introduced when variation is credible.
Liskov Substitution Principle
A subtype must be usable wherever its declared supertype is expected without violating client assumptions. If a ReadOnlyAccount inherits from Account but throws an exception whenever clients call withdraw, the hierarchy may violate substitutability. Composition or separate interfaces may be more honest.
Interface Segregation Principle
Clients should not be forced to depend on methods they do not use. Instead of a large Repository interface containing reporting, deletion, caching, and transaction operations, define focused interfaces for the relevant clients.
Dependency Inversion Principle
High-level policy should not depend directly on low-level details; both should depend on abstractions. Details such as a PostgreSQL repository or a payment SDK implement ports required by application policy. Dependency injection supplies implementations at the composition boundary.
5. Contracts and Invariants
Design by contract expresses what a component promises. A contract can include:
Preconditions: what must be true before an operation.
Postconditions: what the operation guarantees afterward.
Invariants: what remains true for every valid object state.
For example, an operation reserve(stock, quantity) may require quantity > 0 and an available quantity at least equal to the request. Its postcondition is that available stock decreases by the reserved quantity, or the operation reports failure without a partial decrease. The exact transaction behavior must be specified rather than assumed.
Contracts improve API documentation, testing, and collaboration. They also expose ambiguity: whether an operation is idempotent, which errors are possible, whether null values are accepted, and what authorization is required. In distributed systems, contracts should include message schemas, versioning rules, timeout behavior, and retry safety.
A precondition should not be used to hide an ordinary, expected user error. User input should be validated at a suitable boundary, while internal invariants should be protected by domain objects and services.
6. Recognized Design Patterns
A design pattern is a named, reusable solution structure for a recurring design problem. It is not a code template that must be copied unchanged.
Pattern
Problem addressed
Typical full-stack use
Strategy
Select among interchangeable algorithms
Tax, shipping, or pricing policies
Factory
Centralize creation decisions
Constructing payment providers
Adapter
Make incompatible interfaces work together
Wrapping a vendor SDK
Facade
Offer a simple interface to a complex subsystem
Checkout orchestration
Observer / publish–subscribe
Notify dependants of events
Updating projections or sending notifications
Repository
Abstract persistence access
Domain-oriented data access
Decorator
Add behavior without changing the core object
Caching, authorization, metrics
Command
Represent an action as an object or message
Queues, audit logs, undo operations
Patterns have costs. An abstraction can obscure control flow, an event-driven design can complicate tracing, and a repository can become a leaky wrapper around SQL. Choose a pattern because a recurring force justifies it, not because its name appears in a catalogue.
7. Refactoring and Design Quality
Refactoring changes the internal structure of existing code without intentionally changing its externally observable behavior. Examples include extracting a method, renaming a misleading class, replacing duplication with a shared abstraction, and moving behavior to the object that owns the relevant data.
Safe refactoring requires a reliable feedback loop:
Establish characterization tests for important current behavior.
Make one small structural change.
Run unit, integration, and relevant end-to-end tests.
Review the resulting design and commit a coherent change.
Common design smells include duplicated logic, long methods, primitive obsession, feature envy, inappropriate intimacy between classes, speculative generality, and a “god object.” A smell is evidence for investigation, not proof that a particular refactoring is correct. For example, duplication should sometimes remain when two similar pieces are likely to evolve independently.
Automated tests are especially important because refactoring changes structure while preserving behavior. Static analysis, dependency graphs, code review, and architecture decision records provide additional evidence, but none replaces understanding of requirements.
8. Applied Case: An Illustrative Online Bookstore
Background
Consider an illustrative online bookstore called CampusReads. Customers search books, place orders, pay online, and receive shipment notifications. The initial implementation has a React interface, a Node.js server, a relational database, and a payment provider.
Problem
The first version contains a single OrderController that validates HTTP input, calculates discounts, queries the database, calls the payment provider, sends email, and formats responses. A change to discount rules requires edits in several endpoints. Tests require a live database and payment account. Failed payment retries sometimes create duplicate orders.
Analysis
The controller has low cohesion and excessive coupling. It exposes vendor details to business logic, mixes presentation with policy, and lacks a clear contract for retry behavior. The design also has an implicit invariant—an order should not be marked paid twice—but does not enforce it reliably.
A sequence model identifies a clearer flow: the controller accepts a request; an application service loads or creates an order; a domain policy calculates the total; a payment port authorizes payment; a repository commits the order; and an event publisher emits OrderPaid after successful persistence.
Approach
The team introduces:
CheckoutService for use-case orchestration.
Order for order state and transition rules.
DiscountPolicy as a Strategy abstraction.
PaymentGateway as a port, with a vendor-specific Adapter.
OrderRepository for persistence access.
An idempotency key in the checkout contract.
Tests for invalid quantities, repeated requests, declined payments, and successful checkout.
The service contract states that the same idempotency key and request produce the same outcome, subject to the documented retention period. A database uniqueness constraint supports this promise; the application must still handle a conflict safely.
Outcome
Business rules can be tested without HTTP or a real payment provider. Adding a student discount requires a new policy rather than modifications throughout the controller. Vendor replacement affects the adapter and configuration rather than domain code. The system has not eliminated complexity: payment failure, network timeouts, and event delivery still require operational monitoring and reconciliation. However, complexity is located behind explicit boundaries.
Learning points
The most valuable improvement was not the number of classes. It was the clarification of responsibilities, contracts, dependency direction, and failure behavior. UML sequence and component models helped the team discuss the design before implementation. Refactoring was performed incrementally under tests rather than through an unsafe rewrite.
9. Quality, Security, and Contemporary Practice
Design decisions affect quality attributes. Decomposition can improve maintainability and team ownership but may increase latency if it creates excessive network calls. Encapsulation supports security by limiting direct access to sensitive state, but authorization must still be enforced at trustworthy server-side boundaries. Contracts should avoid exposing passwords, payment secrets, or unnecessary personal data.
Privacy-by-design requires data minimization, explicit retention rules, access control, auditability, and appropriate protection in transit and at rest. Models should identify personal data flows and trust boundaries. Threat modeling can complement UML by asking what an attacker can control, what assets require protection, and where validation or authorization is needed.
AI-assisted programming can generate classes, tests, or diagrams, but generated design must be reviewed for hidden coupling, insecure defaults, licensing concerns, hallucinated APIs, and incorrect business assumptions. Developers remain accountable for requirements, security, accessibility, testing, and maintainability. Contemporary teams increasingly combine lightweight diagrams, executable API contracts, architecture tests, continuous integration, and observability rather than relying on static documentation alone.
10. Best-Practice Checklist
Before implementing a significant feature, ask:
What user or business outcome is being modeled?
Which model—use case, sequence, state, component, data, or deployment—will reduce the greatest uncertainty?
What responsibilities belong together, and what should be hidden?
Which dependencies are stable abstractions, and which are volatile details?
What preconditions, postconditions, invariants, and failure outcomes apply?
Can the design be tested without unnecessary infrastructure?
Is a pattern solving a demonstrated problem rather than adding ceremony?
What security, privacy, accessibility, and operational concerns cross the boundary?
How will the design evolve when a likely variation occurs?
What evidence will show that refactoring preserved behavior?
Chapter Summary
Software modeling makes system structure, behavior, data, and deployment assumptions visible. UML provides standardized views such as use-case, class, sequence, state, component, and deployment diagrams, while complementary models such as schemas, API contracts, and threat models address concerns UML alone does not fully capture.
Effective design uses decomposition, abstraction, encapsulation, information hiding, and separation of concerns to control complexity. High cohesion and appropriately low coupling improve changeability, but these are contextual heuristics rather than absolute measurements. SOLID principles guide responsibility, extensibility, substitutability, interface size, and dependency direction. Contracts make assumptions explicit through preconditions, postconditions, invariants, and failure behavior. Patterns such as Strategy, Adapter, Repository, Facade, and Decorator provide reusable structures when their trade-offs are understood. Refactoring, supported by tests and feedback, improves internal design without deliberately changing behavior. Professional design also incorporates security, privacy, accessibility, observability, and careful review of AI-generated artifacts.
Key Terms
Abstraction: Representation that exposes essential behavior while hiding irrelevant detail.
Cohesion: Degree to which responsibilities within a module belong together.
Component model: Description of replaceable software units and their dependencies.
Composition boundary: Location where concrete implementations are assembled and injected.
Contract: Explicit statement of required inputs, guarantees, and valid states.
Coupling: Degree of dependency between software modules.
Decomposition: Division of a system into smaller responsibilities or parts.
Encapsulation: Grouping state and behavior behind a controlled boundary.
Information hiding: Deliberate concealment of design decisions likely to change.
Invariant: Condition that must remain true for every valid object state.
Model: Purposeful representation of selected aspects of a system.
Pattern: Named solution structure for a recurring design problem.
Postcondition: Guarantee expected after an operation completes.
Precondition: Requirement that must hold before an operation begins.
Refactoring: Behavior-preserving improvement to internal code structure.
Separation of concerns: Allocation of distinct responsibilities to distinct units.
SOLID: Five object-oriented design guidelines concerning responsibility, extension, substitution, interfaces, and dependencies.
UML: Standardized language for visualizing and specifying software systems.
Idempotency: Property that repeating an operation produces the same intended outcome as performing it once.
Review Questions
Short Answer Questions
What is the principal purpose of a UML sequence diagram?
Distinguish encapsulation from information hiding.
Define coupling and cohesion.
What are preconditions, postconditions, and invariants?
Why should design patterns not be applied mechanically?
Long Answer Questions
Explain how decomposition, abstraction, and separation of concerns work together in a full-stack application.
Describe each SOLID principle and illustrate its application in an order-processing system.
Compare UML models with complementary models such as database schemas and API specifications.
Explain how contracts improve reliability in distributed applications.
Discuss the role of automated tests in safe refactoring and the limitations of tests as evidence of design quality.
Analytical / Application Questions
A controller validates requests, calculates tax, writes database records, and sends email. Analyze its design using cohesion, coupling, and Single Responsibility reasoning, then propose a decomposition.
Design a contract for a payment operation that may be retried after a network timeout. Identify preconditions, postconditions, failure behavior, and an idempotency strategy.
A team proposes splitting every domain class into a separate network microservice. Evaluate this proposal using decomposition, coupling, cohesion, observability, and operational cost. Recommend conditions under which the split would or would not be justified.
Multiple Choice Questions with Answers
Question 1
Which UML diagram primarily represents the order and timing of messages between participating objects?
A. Class diagram
B. Sequence diagram
C. Deployment diagram
D. Package diagram
Correct Answer: B. Sequence diagram
Question 2
Information hiding is best described as:
A. Putting every field in a public data structure
B. Removing all abstractions from a design
C. Concealing design decisions likely to change
D. Dividing a database into more tables
Correct Answer: C. Concealing design decisions likely to change
Question 3
Which SOLID principle states that high-level policy should not depend directly on low-level details?
A. Single Responsibility Principle
B. Liskov Substitution Principle
C. Interface Segregation Principle
D. Dependency Inversion Principle
Correct Answer: D. Dependency Inversion Principle
Question 4
A class that calculates tax, sends email, formats HTML, and writes SQL most clearly exhibits:
A. High cohesion
B. Low cohesion
C. Information hiding
D. Substitutability
Correct Answer: B. Low cohesion
Question 5
Which contract element describes a condition that must hold before an operation begins?
A. Invariant
B. Postcondition
C. Precondition
D. Adapter
Correct Answer: C. Precondition
Question 6
Which pattern is most appropriate for selecting among interchangeable pricing algorithms?
A. Strategy
B. Repository
C. Facade
D. Observer
Correct Answer: A. Strategy
Question 7
What is the main purpose of refactoring?
A. Change externally observable behavior deliberately
B. Improve internal structure while preserving intended behavior
C. Replace all tests with documentation
D. Increase the number of classes regardless of need
Correct Answer: B. Improve internal structure while preserving intended behavior
Question 8
An adapter is primarily used to:
A. Represent an object’s lifecycle states
B. Make incompatible interfaces work together
C. Persist every domain object automatically
D. Guarantee high cohesion in every module
Correct Answer: B. Make incompatible interfaces work together
Question 9
Which statement best describes a model?
A. A complete replacement for implemented software
B. A purposeful representation of selected system aspects
C. A mandatory UML diagram for every requirement
D. A guarantee that the design contains no defects
Correct Answer: B. A purposeful representation of selected system aspects
Question 10
Why is an idempotency key useful in a retryable checkout operation?
A. It ensures that repeated requests do not unintentionally create repeated outcomes
B. It eliminates the need for authentication
C. It makes every payment operation synchronous
D. It removes the need for database constraints
Correct Answer: A. It ensures that repeated requests do not unintentionally create repeated outcomes