Be Software Architecture Wise
Be Wise Apps

Leer en español · Lire en français · Auf Deutsch lesen

Section 0.3 of 0.8 · Beginner · 16 min read

Functions, modules, objects, interfaces, libraries and frameworks

Video lesson

5 min · narrated, with captions
Listen only (audio)
Read the transcript

Think of a large restaurant. Instead of one cook doing everything, there are stations: grill, pastry and salads, each with its own staff, tools and recipes. Orders pass between stations on tickets, so the grill doesn't need to know how the pastry chef works. Code is organised in just the same way, and this lesson shows how.

The smallest building block is the function: a named piece of code that does one job. Think of a coffee machine. You put in beans, water and a choice of drink, and out comes a coffee. Functions let you write a rule once and reuse it. A shop might write its VAT calculation in one function, and call it from the checkout, the invoice and the refund screen. Then, when the VAT rate changes, one function changes, not twenty scattered copies that would soon drift apart.

Next, related functions are grouped into modules, and related modules into a package. Here, a package called shop holds two modules: pricing and orders. Each module has a small public part, meant for other modules to use, and private details. In the pricing module, the VAT function is public. The helper below it is private, marked in Python by a leading underscore. The orders module calls only the public pricing function, never the private helper. Think of a department with a public front desk and internal processes nobody outside needs to see. The value of this split is that the private part can be rewritten freely without breaking anyone else, as long as the public part keeps its promises.

Many languages let you bundle data together with the functions that work on it. A class is the template, and an object is one thing made from it. A bank account class says every account has an owner and a balance; each customer's account is an object. The key idea is encapsulation. If any code could set the balance directly, nothing would stop it going negative. Instead, the withdraw method is the only door, and it checks the rules every time, like a bank teller in front of the vault.

The most important idea in this lesson is the interface. It's a contract: the operations one part offers to others, what they take, what they return and what can go wrong. A power socket is an interface: any appliance with the right plug works. Here's why it matters. Suppose a shop's checkout calls one payment provider's own functions directly, everywhere. If the shop switches provider next year, the checkout has to be rewritten. Instead, the shop defines its own small payment gateway interface, with one operation: charge this amount to this card. It writes one adapter per provider. The checkout depends only on the interface, so switching provider means writing one new adapter. It makes testing easy too. In tests, a fake gateway that never contacts a real bank can stand in for the real one.

Almost no one writes everything from scratch. A library is code someone else wrote that your code calls when it needs it, such as a library for dates or for making PDF files. Your code stays in charge. A framework is the other way round. It provides the skeleton of the application and calls your code at the right moments. This is called inversion of control: don't call us, we'll call you. Frameworks save a lot of design work, but they are usually hard to replace later.

Every library or framework you use is a dependency, fetched by a package manager such as pip for Python or npm for JavaScript. Here, a shop app chooses three packages directly. But those packages bring in five more of their own, called transitive dependencies. You chose three packages, but you depend on eight. Each one is code you didn't write, running with all the permissions of your own. So a flaw in any one of them, such as this crypto package, is a flaw in your app.

Why does all this structure matter? Because most of the cost of software is spent changing it. Two ideas describe good structure. Coupling is how much one part depends on the details of another. Cohesion is how well the contents of one part belong together. On the left, four modules all reach into each other's data. A change to one can spread to all the others. On the right, the same modules have just three links, each through a small interface. That's the goal: loose coupling and high cohesion. It also lets separate teams work and release independently.

Architecture is this same activity at a larger scale: deciding the main parts, their interfaces and which way dependencies point. The full section has the code and a checklist for adding dependencies. Next, in Section zero point four, the code leaves the single computer, as we look at how computers talk.

What you need to know

Why organise code at all

A small script of twenty lines can be read top to bottom in a minute. A real system, such as a food-delivery platform, may have hundreds of thousands or millions of lines written by dozens of people over many years. Nobody can hold that in their head. The only way to work on such a system is to divide it into parts that can be understood, tested and changed one at a time, with clear rules about how the parts may use each other.

Programming languages give us a ladder of building blocks for this, from small to large: functions, modules and packages, classes and objects, and the interfaces between them. Above the code we write ourselves sit libraries and frameworks written by others. This section climbs that ladder. The same ideas, applied at larger scale, are what software architecture is about.

Functions: name a job, then reuse it

A function is a named piece of code that does one job. It takes inputs (called parameters or arguments), does some work and usually gives back an output (its return value). Think of a coffee machine: you put in beans, water and a choice of drink; it does its work; out comes a coffee. You do not need to know how the pump works to use it.

Functions bring three benefits. Reuse: the VAT calculation is written once and called from the checkout, the invoice and the refund screen. Readability: total_with_vat(basket) says what is happening without showing how. Change in one place: when the VAT rate changes, one function changes, not twenty copies of the same arithmetic scattered through the code (copies that would soon drift apart).

A good function does one thing, has a name that says what it does, takes the inputs it needs as parameters rather than reaching out for hidden data, and is short enough to read at a glance. A function that only computes its output from its inputs, without changing anything else, is called a pure function; pure functions are especially easy to test, because the same inputs always give the same output.

VAT_RATE = 0.20

def total_with_vat(prices_in_pence):
    """Return the total of a basket in pence, including VAT."""
    subtotal = sum(prices_in_pence)
    return round(subtotal * (1 + VAT_RATE))

checkout_total = total_with_vat([1250, 399])   # 1979
invoice_total = total_with_vat([5000])         # 6000
One function, reused in several places

A module groups related functions and data into a unit with its own name. In Python a module is simply one .py file; pricing.py might hold everything to do with prices, discounts and tax. Another file uses it by writing import pricing. A package is a folder of related modules, so a shop might have a shop package containing pricing, orders and customers modules. Other languages use different words (Java has packages, C# has namespaces and assemblies, JavaScript has modules), but the idea is the same.

A module is like a department in an organisation. The finance department has a public front desk where others can make requests ("please raise this invoice"), and internal processes nobody outside needs to see. Modules similarly separate a small public part, meant for other modules to use, from private details. Python marks private names by convention with a leading underscore, such as _round_pence; Java and C# enforce privacy with keywords such as private. The value of the split is that the private part can be rewritten freely without breaking anyone else, as long as the public part keeps its promises.

Functions, modules and a package

A package called shop contains two modules, pricing and orders; each module has a public function and a private helper, and orders calls the public pricing functionPackage: shopModule: pricing.pytotal_with_vat(items)public: other modules call this_round_pence(amount)private helper (leading underscore)Module: orders.pyplace_order(customer, items)calls total_with_vat_save(order)private helperFunctions live in modules; related modules are grouped into a package; only public names are meant to be used from outside
The orders module uses only the public function of the pricing module, never its private helper.

Classes and objects: data with its behaviour

Many languages let you bundle data together with the functions that work on it. A class is the template; an object is one thing made from it. A BankAccount class might say that every account has an owner and a balance, and can accept deposits and withdrawals. Each customer's account is then a separate object made from that class. Functions that belong to a class are called methods. (See the glossary entry for class and object.)

The most useful idea here for architecture is encapsulation: an object keeps its data private and lets other code change it only through its methods. If anyone could set account.balance directly, nothing would stop code elsewhere from making a balance negative or skipping the audit log. With encapsulation, the withdraw method is the only door, and it can enforce the rules every time. A bank teller is the analogy: customers cannot walk into the vault, but they can ask the teller, who checks the rules before handing over money.

There is much more to object-oriented programming (inheritance, polymorphism, design patterns), and practitioners disagree about how much of it is helpful: some languages, such as Go, deliberately leave out inheritance, and many teams favour a more functional style. Encapsulation, however, is valuable in every style.

class BankAccount:
    def __init__(self, owner):
        self.owner = owner
        self._balance = 0          # private by convention

    def deposit(self, pence):
        if pence <= 0:
            raise ValueError("Deposit must be positive")
        self._balance += pence

    def withdraw(self, pence):
        if pence > self._balance:
            raise ValueError("Insufficient funds")
        self._balance -= pence

    def balance(self):
        return self._balance

acct = BankAccount("Ana")
acct.deposit(10000)
acct.withdraw(2500)
print(acct.balance())   # 7500
Encapsulation: the balance can only change through methods that enforce the rules

Interfaces: contracts between parts

An interface is the agreed set of operations one part of a system offers to others: their names, what inputs they take, what they return and what can go wrong. It is a contract. The caller relies on the contract, not on how the work is done. A power socket is an interface: any appliance with the right plug works, and the appliance does not care whether the electricity comes from wind, gas or nuclear power.

In code, an interface lets you swap one implementation for another. Suppose a shop takes card payments through one payment provider today but may switch providers next year. If the checkout code calls the provider's own functions directly, everywhere, switching means rewriting the checkout. If instead the shop defines its own small PaymentGateway interface with one operation, charge(amount, card_token), then writes one adapter per provider, the checkout depends only on the interface and switching providers means writing one new adapter. The same trick makes testing easy: in tests, a fake gateway that never contacts a real bank can stand in for the real one.

Interfaces are the most important idea in this section for architects. Every boundary in a system, from a function signature to a web API used by thousands of other companies, is an interface. Good ones are small, stable and hide decisions that are likely to change; that principle, called information hiding, was set out by David Parnas in 1972 and is covered in Chapter 3.

from typing import Protocol

class PaymentGateway(Protocol):
    def charge(self, amount_pence: int, card_token: str) -> bool: ...

class ProviderAGateway:
    def charge(self, amount_pence, card_token):
        ...  # call Provider A's web API
        return True

class FakeGateway:                      # used in tests
    def charge(self, amount_pence, card_token):
        return amount_pence < 100_000   # pretend large payments fail

def checkout(basket_total, card_token, gateway: PaymentGateway):
    return "paid" if gateway.charge(basket_total, card_token) else "declined"
An interface (here a Python Protocol) with two interchangeable implementations

Libraries and frameworks: code written by others

Almost no one writes everything from scratch. A library is reusable code that someone else has written and that your code calls when it needs it: a library for handling dates and time zones, for generating PDFs, for talking to a database. You stay in charge; the library is a tool you pick up and put down.

A framework is different. It provides the overall skeleton of an application and decides when things happen; you fill in the gaps with your own code, and the framework calls that code at the right moments. A web framework such as Django (Python), Spring (Java), Ruby on Rails or Express (JavaScript) receives each incoming web request, works out which of your functions should handle it, calls that function and turns its result into a web page or data. The short way to remember the difference: you call a library; a framework calls your code.

That reversal is called inversion of control. Martin Fowler credits Ralph Johnson and Brian Foote with using the term in 1988, and the idea is often summed up as the Hollywood principle: Don't call us, we'll call you. A restaurant franchise is a fair analogy: the franchise supplies the building, the ordering system, the opening hours and the rules; the franchisee supplies the cooking in the slots the franchise provides.

Who calls whom

With a library your code calls the library; with a framework the framework calls your code, which is inversion of controlLibrary: your code is in chargeYour codedecides what happens whenLibrarye.g. a date or PDF libraryyou call itit returnsFramework: it is in chargeFrameworkruns the main loopYour codefills in the gapsit calls youyou return
The direction of control is the real difference between a library and a framework.

Library or framework

LibraryFramework
Who controls the flowYour codeThe framework (inversion of control)
ExamplesA date library, a PDF generator, a JSON parserDjango, Spring, Ruby on Rails, Angular
Ease of replacing laterUsually moderate: change the places that call itUsually hard: the framework's structure shapes the whole application
Main benefitSaves writing one capability yourselfSaves designing the whole structure; consistent conventions across a team
Main riskOne more dependency to maintainLock-in to its way of working and its upgrade cycle

Dependencies and package managers

Every library or framework your code needs is a dependency. Modern projects have many, so they use a package manager to fetch them: pip for Python (from the Python Package Index, PyPI) and npm for JavaScript (from the npm registry) are the best known, with equivalents such as Maven for Java and NuGet for .NET. A project lists what it needs, often with version ranges, and the package manager downloads those packages and, crucially, the packages they depend on. These indirect, transitive dependencies often outnumber the direct ones many times over.

Versions are commonly numbered MAJOR.MINOR.PATCH under a convention called semantic versioning: a change to the major number signals a breaking change, a minor number new features that should not break existing users, and a patch number bug fixes. Projects also keep a lock file (such as package-lock.json for npm) recording the exact version of every package installed, so that every developer's machine and the production build use the same code.

Direct and transitive dependencies

An application with three direct dependencies, which bring in five transitive dependencies of their ownYour shop appweb frameworkdirect dependencypayments SDKdirect dependencyPDF librarydirect dependencytemplatingtransitiveHTTP clienttransitiveJSON parsertransitivecryptotransitivefontstransitiveYou chose three packages; you depend on eight. A flaw in any one (for example crypto) is a flaw in your app.
Most of the code your application runs was chosen by someone else's choices.

Dependencies save enormous effort, but each one is code you did not write, running with all the permissions of your own. Two well-known incidents show the risks. In March 2016 a developer removed his packages from npm in a dispute, among them left-pad, a tiny function of about eleven lines for padding text; thousands of projects that depended on it, directly or indirectly, suddenly failed to build until npm restored it. In December 2021 a severe security flaw, nicknamed Log4Shell, was disclosed in Log4j 2, a widely used Java logging library; organisations around the world spent weeks finding every system that contained it, often buried several levels deep in their dependencies.

Checklist: before adding a dependency

Questions a careful team asks before relying on someone else's package.

  • Do we need it?A few lines of our own code may be cheaper over time than a package that must be tracked and updated.
  • Is it maintained?Look at recent releases, open security issues and how many people maintain it. An abandoned package becomes your problem.
  • What does it bring with it?Check how many transitive dependencies it adds; each is more code to trust.
  • Is the licence compatible?Open-source licences differ; some impose obligations when you distribute your software.
  • Are versions pinned and scanned?Use a lock file and an automated tool that warns when a known vulnerability affects a version you use.
  • Could we replace it?Wrap important dependencies behind our own interface, so a change of supplier touches one place.

Why structure matters: change and teams

Code is read and changed far more often than it is first written. Most of the lifetime cost of software is spent changing it: fixing bugs, adding features, adapting to new laws and new partners. Structure decides how expensive that is. In a well-organised system, a change to how VAT is calculated touches one module. In a badly organised one, it touches forty files owned by five teams, and something unrelated breaks.

Two ideas, first described by Larry Constantine and colleagues in the structured design movement of the late 1960s and 1970s, capture what "well organised" means. Coupling is how much one part depends on the details of another: in a tightly coupled system, changing one part forces changes in others. Cohesion is how well the contents of one part belong together: a highly cohesive module does one clear job. The goal is loose coupling and high cohesion: parts that each do one job well and talk to each other through small, stable interfaces.

Structure also matters for people. If two teams must both edit the same module for most of their work, they will constantly wait for and break each other. If each team owns cohesive modules with clear interfaces, they can work and release independently. Melvin Conway observed in 1968 that systems tend to mirror the communication structure of the organisations that build them, an observation known as Conway's law and explored in Chapter 3.

Coupling at a glance

On the left four modules are all linked to each other; on the right the same modules have only three links, each through an interfaceTightly coupled, low cohesionOrdersBillingStockEmaileach module reaches into the others' dataLoosely coupled, high cohesionOrdersBillingStockEmailinterfacefew links, each through a small interface
The same four modules, connected in two ways: every extra link is another path along which a change can spread.

Why this matters for architecture

Architecture is largely the same activity as this section, done at a larger scale and with higher stakes: deciding what the main parts of a system are, what each is responsible for, which interfaces connect them and which way dependencies point. A module inside one program and a separately deployed service across a network are both parts with interfaces; the questions of coupling, cohesion and information hiding apply to both.

Chapter 3 makes these principles precise, starting with modularity and coupling and cohesion, and Chapter 8 returns to dependencies as a security matter in supply-chain security. Next, the code leaves the single computer: the following section explains how computers talk to each other.

Check your understanding

Why is it better to call one total_with_vat function from three places than to write the calculation three times?

The rule then lives in one place: when the VAT rate changes, one change fixes every caller, and the copies cannot drift apart.

What does encapsulation stop other code from doing to a bank account object?

It stops other code changing the balance directly, so every change has to go through methods such as withdraw that enforce the rules.

A web framework calls your function whenever a request for /orders arrives. What is this arrangement called?

Inversion of control: the framework, not your code, controls the flow and calls your code when needed.

What is a lock file, such as package-lock.json, for?

It records the exact version of every package installed, including indirect ones, so every developer machine and every production build use exactly the same code.

Next · 0.4How computers talkHow data crosses the internet in packets, what IP addresses, ports, TCP, UDP and DNS do, how HTTP requests and HTTPS work, why latency has a floor set by physics, and what happens when you type a web address.Continue →

Sources and further reading

Have a question about this section? ArchitectBot, the course’s AI study assistant, answers from the course first.

Ask: “How do I decide whether to build a feature on a framework or keep my business logic independent of it?”