Intro
Enterprise systems need to manage complexity — of data access, business rules, and change. Martin Fowler’s Catalog of Patterns of Enterprise Application Architecture collects practical patterns used to structure enterprise apps. One of the most widely used patterns from the catalog is the Repository Pattern.
This article explains the Repository Pattern, why it matters for enterprise applications (especially for maintainability and testability), and provides a small but realistic Python implementation that you can deploy and demo. The demo models a simplified Student Enrollment service (common in education platforms).
What is the Repository Pattern?
The Repository Pattern mediates between the domain and data mapping layers using a collection-like …
Intro
Enterprise systems need to manage complexity — of data access, business rules, and change. Martin Fowler’s Catalog of Patterns of Enterprise Application Architecture collects practical patterns used to structure enterprise apps. One of the most widely used patterns from the catalog is the Repository Pattern.
This article explains the Repository Pattern, why it matters for enterprise applications (especially for maintainability and testability), and provides a small but realistic Python implementation that you can deploy and demo. The demo models a simplified Student Enrollment service (common in education platforms).
What is the Repository Pattern?
The Repository Pattern mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects. It hides the details of data access (SQL, ORM queries, remote APIs), enabling:
- Separation of concerns (business logic doesn’t talk SQL)
- Easier unit testing (swap repository with in-memory stub or mock)
- Centralized location for querying logic and caching
- Clearer domain model APIs
In Fowler’s words: repositories are like an in-memory collection of domain objects.
When to use it
- When your domain logic needs a persistence-agnostic API.
- When you want to centralize queries and data mapping.
- To facilitate unit testing by allowing repository substitution.
Avoid over-abstracting if your app is trivial — sometimes using an ORM directly is fine.
Real-world example overview
We’ll implement a StudentRepository that handles CRUD for Student entities and a small Flask HTTP API to demo:
GET /students— list studentsPOST /students— create a studentGET /students/<id>— get studentPUT /students/<id>— update studentDELETE /students/<id>— delete student
We provide:
-
A repository interface with two implementations:
-
SqliteStudentRepository(using SQLite viasqlite3) — minimal dependencies -
InMemoryStudentRepository— for testing and quick demos
Why this example matters (enterprise angle)
Education platforms (LMS, student information systems) must adapt to changing data stores and scale. The repository pattern enables switching storage, adding caching, or moving specific queries into optimized implementations without touching business logic — crucial in enterprise maintenance cycles.
Code (full, runnable example)
Save these files in a directory called
student_repo_demo/
models.py
# models.py
from dataclasses import dataclass
from typing import Optional
@dataclass
class Student:
id: Optional[int]
name: str
email: str
### `repositories.py`
# repositories.py
import sqlite3
from typing import List, Optional
from models import Student
import os
class StudentRepository:
def list(self) -> List[Student]:
raise NotImplementedError
def get(self, student_id: int) -> Optional[Student]:
raise NotImplementedError
def add(self, student: Student) -> Student:
raise NotImplementedError
def update(self, student: Student) -> None:
raise NotImplementedError
def delete(self, student_id: int) -> None:
raise NotImplementedError
class SqliteStudentRepository(StudentRepository):
def __init__(self, db_path='students.db'):
self.db_path = db_path
self._ensure_table()
def _conn(self):
return sqlite3.connect(self.db_path)
def _ensure_table(self):
with self._conn() as c:
c.execute('''
CREATE TABLE IF NOT EXISTS students (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
);
''')
def list(self):
with self._conn() as c:
rows = c.execute('SELECT id, name, email FROM students').fetchall()
return [Student(id=r[0], name=r[1], email=r[2]) for r in rows]
def get(self, student_id):
with self._conn() as c:
r = c.execute('SELECT id, name, email FROM students WHERE id=?', (student_id,)).fetchone()
return Student(id=r[0], name=r[1], email=r[2]) if r else None
def add(self, student: Student):
with self._conn() as c:
cur = c.execute('INSERT INTO students (name, email) VALUES (?, ?)', (student.name, student.email))
student.id = cur.lastrowid
return student
def update(self, student: Student):
with self._conn() as c:
c.execute('UPDATE students SET name=?, email=? WHERE id=?', (student.name, student.email, student.id))
def delete(self, student_id: int):
with self._conn() as c:
c.execute('DELETE FROM students WHERE id=?', (student_id,))
### `service.py`
from repositories import StudentRepository
from models import Student
from typing import List
class StudentService:
def __init__(self, repo: StudentRepository):
self.repo = repo
def list_students(self) -> List[Student]:
return self.repo.list()
def create_student(self, name: str, email: str) -> Student:
s = Student(id=None, name=name, email=email)
return self.repo.add(s)
def get_student(self, student_id: int) -> Student:
return self.repo.get(student_id)
def update_student(self, student_id: int, name: str, email: str):
s = Student(id=student_id, name=name, email=email)
self.repo.update(s)
def delete_student(self, student_id: int):
self.repo.delete(student_id)
app.py
app.py
from flask import Flask, jsonify, request, abort from repositories import SqliteStudentRepository from service import StudentService
app = Flask(name) repo = SqliteStudentRepository(db_path=‘students.db’) service = StudentService(repo)
@app.route(‘/students’, methods=[‘GET’]) def list_students(): students = service.list_students() return jsonify([s.dict for s in students])
@app.route(‘/students’, methods=[‘POST’]) def create_student(): data = request.json if not data or ‘name’ not in data or ‘email’ not in data: abort(400) s = service.create_student(data[‘name’], data[‘email’]) return jsonify(s.dict), 201
@app.route(‘/students/int:student_id’, methods=[‘GET’]) def get_student(student_id): s = service.get_student(student_id) if not s: abort(404) return jsonify(s.dict)
@app.route(‘/students/int:student_id’, methods=[‘PUT’]) def update_student(student_id): data = request.json if not data or ‘name’ not in data or ‘email’ not in data: abort(400) service.update_student(student_id, data[‘name’], data[‘email’]) return ‘’, 204
@app.route(‘/students/int:student_id’, methods=[‘DELETE’]) def delete_student(student_id): service.delete_student(student_id) return ‘’, 204
if name == ‘main’: app.run(debug=True)