Complete guide for migrating existing code to the new architecture
This guide helps you migrate existing routes and code to use the new service layer, repository pattern, and other improvements.
- Routes with business logic
- Direct model queries
- Manual validation
- Inconsistent error handling
- N+1 query problems
- Identify business logic
- Extract to service methods
- Use existing services or create new ones
- Replace direct queries with repository calls
- Use eager loading to prevent N+1 queries
- Leverage repository methods
- Use schemas for API endpoints
- Use validation utilities for forms
- Add proper error handling
- Mock repositories in unit tests
- Test services independently
- Add integration tests
Before:
@route('/timer/start')
def start_timer():
project = Project.query.get(project_id)
if not project:
return error
timer = TimeEntry(...)
db.session.add(timer)
db.session.commit()After:
@route('/timer/start')
def start_timer():
service = TimeTrackingService()
result = service.start_timer(user_id, project_id)
if result['success']:
return success_response(result['timer'])
return error_response(result['message'])Before:
@route('/projects')
def list_projects():
projects = Project.query.filter_by(status='active').all()
# N+1 query when accessing project.client
return render_template('projects/list.html', projects=projects)After:
@route('/projects')
def list_projects():
repo = ProjectRepository()
projects = repo.get_active_projects(include_relations=True)
# Client eagerly loaded - no N+1 queries
return render_template('projects/list.html', projects=projects)Before:
@api.route('/projects', methods=['POST'])
def create_project():
data = request.get_json()
if not data.get('name'):
return jsonify({'error': 'Name required'}), 400
project = Project(name=data['name'], ...)
db.session.add(project)
db.session.commit()
return jsonify(project.to_dict()), 201After:
@api.route('/projects', methods=['POST'])
def create_project():
from app.schemas import ProjectCreateSchema
from app.utils.api_responses import created_response, validation_error_response
schema = ProjectCreateSchema()
try:
data = schema.load(request.get_json())
except ValidationError as err:
return validation_error_response(err.messages)
service = ProjectService()
result = service.create_project(
name=data['name'],
client_id=data['client_id'],
created_by=current_user.id
)
if result['success']:
return created_response(result['project'].to_dict())
return error_response(result['message'])start_timer()- Start a timerstop_timer()- Stop active timercreate_manual_entry()- Create manual entryget_user_entries()- Get user's entriesdelete_entry()- Delete entry
create_project()- Create projectupdate_project()- Update projectarchive_project()- Archive projectget_active_projects()- Get active projects
create_invoice_from_time_entries()- Create invoice from entriesmark_as_sent()- Mark invoice as sentmark_as_paid()- Mark invoice as paid
create_task()- Create taskupdate_task()- Update taskget_project_tasks()- Get project tasks
create_expense()- Create expenseget_project_expenses()- Get project expensesget_total_expenses()- Get total expenses
create_client()- Create clientupdate_client()- Update clientget_active_clients()- Get active clients
get_time_summary()- Get time summaryget_project_summary()- Get project summaryget_user_productivity()- Get user productivity
get_dashboard_stats()- Get dashboard statsget_trends()- Get time trends
All repositories extend BaseRepository with common methods:
get_by_id()- Get by IDget_all()- Get all with paginationfind_by()- Find by criteriacreate()- Create newupdate()- Update existingdelete()- Deletecount()- Count recordsexists()- Check existence
TimeEntryRepository:
get_active_timer()- Get active timerget_by_user()- Get user entriesget_by_project()- Get project entriesget_by_date_range()- Get by date rangeget_billable_entries()- Get billable entriescreate_timer()- Create timercreate_manual_entry()- Create manual entryget_total_duration()- Get total duration
ProjectRepository:
get_active_projects()- Get active projectsget_by_client()- Get client projectsget_with_stats()- Get with statisticsarchive()- Archive projectunarchive()- Unarchive project
InvoiceRepository:
get_by_project()- Get project invoicesget_by_client()- Get client invoicesget_by_status()- Get by statusget_overdue()- Get overdue invoicesgenerate_invoice_number()- Generate numbermark_as_sent()- Mark as sentmark_as_paid()- Mark as paid
TaskRepository:
get_by_project()- Get project tasksget_by_assignee()- Get assigned tasksget_by_status()- Get by statusget_overdue()- Get overdue tasks
ExpenseRepository:
get_by_project()- Get project expensesget_billable()- Get billable expensesget_total_amount()- Get total amount
from app.schemas import ProjectCreateSchema
from app.utils.api_responses import validation_error_response
@api.route('/projects', methods=['POST'])
def create_project():
schema = ProjectCreateSchema()
try:
data = schema.load(request.get_json())
except ValidationError as err:
return validation_error_response(err.messages)
# Use validated data...from app.schemas import ProjectSchema
schema = ProjectSchema()
return schema.dump(project)from app.utils.event_bus import emit_event
from app.constants import WebhookEvent
emit_event(WebhookEvent.TIME_ENTRY_CREATED.value, {
'entry_id': entry.id,
'user_id': user_id
})from app.utils.event_bus import subscribe_to_event
@subscribe_to_event('time_entry.created')
def handle_time_entry_created(event_type, data):
# Handle event
passfrom app.utils.transactions import transactional
@transactional
def create_something():
# Auto-commits on success, rolls back on exception
passfrom app.utils.transactions import Transaction
with Transaction():
# Database operations
# Auto-commits on success, rolls back on exception
pass# Bad - N+1 queries
projects = Project.query.all()
for p in projects:
print(p.client.name) # N+1 query
# Good - Eager loading
from app.utils.query_optimization import eager_load_relations
query = Project.query
query = eager_load_relations(query, Project, ['client'])
projects = query.all()# Repository methods already use eager loading
repo = ProjectRepository()
projects = repo.get_active_projects(include_relations=True)from app.utils.cache import cached
@cached(ttl=3600)
def expensive_operation():
# Result cached for 1 hour
passdef test_service():
service = TimeTrackingService()
service.time_entry_repo = Mock()
service.project_repo = Mock()
result = service.start_timer(user_id=1, project_id=1)
assert result['success'] == Truedef test_repository(db_session):
repo = TimeEntryRepository()
timer = repo.create_timer(user_id=1, project_id=1)
db_session.commit()
active = repo.get_active_timer(1)
assert active.id == timer.idservice = ResourceService()
result = service.create_resource(**data)
if result['success']:
return success_response(result['resource'])
return error_response(result['message'])repo = ResourceRepository()
resources = repo.get_all(limit=50, offset=0, include_relations=True)
return paginated_response(resources, page=1, per_page=50, total=100)service = ResourceService()
result = service.update_resource(resource_id, user_id, **updates)
if result['success']:
return success_response(result['resource'])
return error_response(result['message'])- Timer routes - Core functionality
- Invoice routes - Business critical
- Project routes - Frequently used
- API endpoints - External integration
- Task routes
- Expense routes
- Client routes
- Report routes
- Admin routes
- Settings routes
- User routes
- Always use services for business logic
- Always use repositories for data access
- Always use schemas for API validation
- Always use response helpers for API responses
- Always use constants instead of magic strings
- Always eager load relations to prevent N+1
- Always emit domain events for side effects
- Always handle errors consistently
- Quick Start:
QUICK_START_ARCHITECTURE.md - Full Analysis:
PROJECT_ANALYSIS_AND_IMPROVEMENTS.md - Implementation:
IMPLEMENTATION_SUMMARY.md - Examples: Check
*_refactored.pyfiles
Happy migrating! π