DEV Community

Enterprise Design Patterns: The Repository Pattern (Catalog of Patterns of EAA) — A Practical Guide with Python

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 students
  • POST /students — create a student
  • GET /students/<id> — get student
  • PUT /students/<id> — update student
  • DELETE /students/<id> — delete student

We provide:

  • A repository interface with two implementations:
    • SqliteStudentRepository (using SQLite via sqlite3) — 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)
Enter fullscreen mode Exit fullscreen mode

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)

Top comments (0)