Date: April 18, 2025
Purpose: Guide a team of 3 junior developers to build and deploy TravelBucks, a multi-currency budgeting app for travelers, in 2 weeks.
Audience: Developers with 0-1 year experience, project lead, stakeholders.
TravelBucks helps travelers like Alex, a student in Tokyo, track spending in foreign currencies (e.g., JPY) against a home currency budget (e.g., USD). The app converts transactions in real-time using daily-cached rates and displays a dual-currency dashboard.
- Login/Creation: Email and password setup.
- Currency Setup: Select home currency and foreign currency (search + flags).
- Budget Setup: Define categories (e.g., food, travel) and amounts.
- Dashboard: Donut chart of budget, transaction list in JPY and USD.
- Transactions: Add/edit/delete transactions with auto-conversion.
- Deferred: Dark mode, predictive insights, premium features (Stripe).
- Backend: Django (Python), Django REST Framework (DRF)
- Frontend: React Native
- Database: PostgreSQL
- Currency API: exchangerateapi.com (free)
- Cache: Redis
- Deployment: Heroku
- Version Control: GitHub
- Monitoring: Sentry, JSON logging
- Users: 10–50 daily active users, ~30 transactions/day.
- Team: 3 junior developers (0-1 year experience).
- Timeline: 2 weeks (2 x 1-week sprints).
- Compliance: GDPR (user data), PCI-DSS (future Stripe).
Structure: Layered for simplicity—DRF ViewSet → Service Layer → Repository → PostgreSQL, with Redis for caching.
User → DRF ViewSet → Service Layer → Repository → PostgreSQL
↓
Cache Layer (Redis)
Key Components:
- Models: Users, budgets, transactions.
# travelbucks/models.py from django.db import models class User(models.Model): email = models.EmailField(unique=True) password = models.CharField(max_length=255) home_currency = models.CharField(max_length=3) class Budget(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) category = models.CharField(max_length=50) amount = models.DecimalField(max_digits=10, decimal_places=2) currency = models.CharField(max_length=3) class Transaction(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) amount = models.DecimalField(max_digits=10, decimal_places=2) converted_amount = models.DecimalField(max_digits=10, decimal_places=2) from_currency = models.CharField(max_length=3) to_currency = models.CharField(max_length=3) category = models.CharField(max_length=50) created_at = models.DateTimeField(auto_now_add=True)
- Service Layer: Handles business logic (e.g., currency conversion).
# travelbucks/services/conversion.py import requests from django.core.cache import cache class DailyConverter: CACHE_KEY = "fx_rates" TTL = 24 * 60 * 60 # 24 hours @classmethod def get_rate(cls, from_ccy: str, to_ccy: str) -> float: rates = cache.get(cls.CACHE_KEY) if not rates: resp = requests.get("https://api.exchangerateapi.com/v4/latest/USD") rates = resp.json()["rates"] cache.set(cls.CACHE_KEY, rates, cls.TTL) return rates[to_ccy] / rates[from_ccy] @classmethod def convert(cls, amount: float, from_ccy: str, to_ccy: str) -> float: rate = cls.get_rate(from_ccy, to_ccy) return round(amount * rate, 2)
# travelbucks/services/budget.py from .conversion import DailyConverter from .repositories.transaction import TransactionRepo class BudgetService: def __init__(self, user): self.user = user self.transaction_repo = TransactionRepo() def create_transaction(self, amount: float, from_currency: str, to_currency: str, category: str): converted_amount = DailyConverter.convert(amount, from_currency, to_currency) self.transaction_repo.create( user=self.user, amount=amount, converted_amount=converted_amount, from_currency=from_currency, to_currency=to_currency, category=category )
- Repository: Abstracts database access.
# travelbucks/repositories/transaction.py from django.db.models import Model class TransactionRepo: def __init__(self, model: Model = Transaction): self.model = model def create(self, **attrs): return self.model.objects.create(**attrs) def list_for_user(self, user, **filters): return self.model.objects.filter(user=user, **filters)
- API Endpoints: Simple DRF ViewSets.
# travelbucks/views/transaction.py from rest_framework.viewsets import ModelViewSet from .models import Transaction from .serializers import TransactionSerializer class TransactionViewSet(ModelViewSet): queryset = Transaction.objects.all() serializer_class = TransactionSerializer def perform_create(self, serializer): serializer.save(user=self.request.user)
Structure: Beginner-friendly layout.
/mobile/src
├── components/ # Button, DonutChart
├── screens/ # LoginScreen, DashboardScreen
├── hooks/ # useAuth, useBudget
├── store/ # Redux slices
├── services/ # API calls (axios)
Key Components:
- State Management: Redux Toolkit for rates, React Query for transactions.
// mobile/src/store/ratesSlice.js import { createSlice } from '@reduxjs/toolkit'; const ratesSlice = createSlice({ name: 'rates', initialState: { rates: {}, lastFetched: null }, reducers: { setRates(state, action) { state.rates = action.payload; state.lastFetched = Date.now(); } } }); export const { setRates } = ratesSlice.actions; export default ratesSlice.reducer;
// mobile/src/store/index.js import { configureStore } from '@reduxjs/toolkit'; import ratesReducer from './ratesSlice'; import budgetReducer from './budgetSlice'; export const store = configureStore({ reducer: { rates: ratesReducer, budget: budgetReducer } });
- API Service: Axios for API calls.
// mobile/src/services/api.js import axios from 'axios'; import { store } from '../store'; import { setRates } from '../store/ratesSlice'; const api = axios.create({ baseURL: 'http://localhost:8000/api' }); export const fetchRates = async () => { const response = await api.get('/rates'); store.dispatch(setRates(response.data)); return response.data; }; export const fetchTransactions = async () => { const response = await api.get('/transactions'); return response.data; };
- Donut Chart: Budget visualization.
// mobile/src/components/DonutChart.js import { PieChart } from 'react-native-chart-kit'; import { useSelector } from 'react-redux'; import { Dimensions } from 'react-native'; const DonutChart = () => { const budget = useSelector(state => state.budget); const data = budget.categories.map(cat => ({ name: cat.name, amount: cat.spent, color: cat.color || '#'+(Math.random()*0xFFFFFF<<0).toString(16), legendFontColor: '#7F7F7F' })); return ( <PieChart data={data} width={Dimensions.get('window').width - 40} height={220} chartConfig={{ color: () => '#000' }} accessor="amount" backgroundColor="transparent" paddingLeft="15" testID="pie-chart" /> ); };
- Dashboard: Combines donut chart and transaction list.
// mobile/src/screens/DashboardScreen.js import { FlatList, Text, View } from 'react-native'; import { useQuery } from '@tanstack/react-query'; import { fetchTransactions } from '../services/api'; import DonutChart from '../components/DonutChart'; const DashboardScreen = () => { const { data: transactions } = useQuery(['transactions'], fetchTransactions); return ( <View> <DonutChart /> <FlatList data={transactions} renderItem={({ item }) => ( <Text>{item.amount} {item.from_currency} ({item.converted_amount} {item.to_currency})</Text> )} keyExtractor={item => item.id.toString()} /> </View> ); };
- Backend: Cache rates in Redis (24-hour TTL). Index
user_idandcreated_atin PostgreSQL. - Frontend: Use
useMemofor donut chart calculations,FlatListfor transactions.
- Clone Repo:
git clone https://github.com/your-org/travelbucks.git
- Backend Setup:
cd backend docker-compose up- Creates PostgreSQL, Redis, and Django services.
- Runs at
http://localhost:8000.
- Frontend Setup:
cd mobile npm install npm start- Starts React Native at
http://localhost:3000.
- Starts React Native at
- Environment Variables:
- Add
EXCHANGERATE_API_KEYtobackend/.env(get from exchangerateapi.com).
- Add
Docker Compose:
version: "3.8"
services:
db:
image: postgres:13
environment:
POSTGRES_DB: travelbucks
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
ports: ["5432:5432"]
redis:
image: redis:6
ports: ["6379:6379"]
web:
build: ./backend
ports: ["8000:8000"]
depends_on: ["db", "redis"]
environment:
DATABASE_URL: postgres://user:pass@db:5432/travelbucks
CACHE_URL: redis://redis:6379/0
EXCHANGERATE_API_KEY: your_api_key
mobile:
build: ./mobile
ports: ["3000:3000"]
depends_on: ["web"]Backend Dockerfile:
FROM python:3.10-slim
WORKDIR /app
COPY requirements.txt ./
RUN pip install -r requirements.txt
COPY . .
CMD ["gunicorn", "travelbucks.wsgi:application", "--bind", "0.0.0.0:8000"]Mobile Dockerfile:
FROM node:16
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
CMD ["npm", "start"]Requirements (backend/requirements.txt):
django==4.2
djangorestframework==3.14
psycopg2-binary==2.9
requests==2.28
django-redis==5.2
gunicorn==20.1
pytest==7.4
pytest-django==4.5
Package.json (mobile/package.json):
{
"dependencies": {
"@reduxjs/toolkit": "^1.9",
"@tanstack/react-query": "^4.29",
"axios": "^1.4",
"react-native": "^0.71",
"react-native-chart-kit": "^6.12",
"react-redux": "^8.0"
},
"devDependencies": {
"@testing-library/react-native": "^12.1",
"jest": "^29.5",
"eslint": "^8.40"
}
}- Create Heroku app:
heroku create travelbucks-prod. - Add add-ons:
heroku addons:create heroku-postgresql:hobby-dev,heroku addons:create heroku-redis:hobby-dev. - Set environment variables:
heroku config:set DATABASE_URL=<postgres-url> heroku config:set CACHE_URL=<redis-url> heroku config:set EXCHANGERATE_API_KEY=<your-key>
- Deploy via GitHub Actions (see CI/CD below).
Timeline: 2 x 1-week sprints, daily tasks for 3 developers (D1, D2, D3).
Goal: Deliver MVP (login, currency setup, budget, dashboard, transactions) by April 30, 2025.
Mentoring: Daily 15-minute check-ins, 1-hour pair programming weekly, 30-minute Lunch & Learn weekly.
Goal: Setup repo, build login, currency conversion, and transaction backend.
-
Day 1:
- D1: Create GitHub repo (
travelbucks), addbackendandmobilefolders, setupdocker-compose.yml, and GitHub Actions.git init git add . git commit -m "feat: initial repo setup" git push origin main
- D2: Implement
Usermodel, login API (/api/login), and password hashing.# travelbucks/views/user.py from django.contrib.auth.hashers import make_password, check_password from rest_framework.views import APIView from rest_framework.response import Response class LoginView(APIView): def post(self, request): user = User.objects.get(email=request.data['email']) if check_password(request.data['password'], user.password): return Response({"message": "Logged in"}) return Response({"error": "Invalid credentials"}, status=401)
- D3: Write
README.mdand setup Swagger UI (/docs/). - Mentor: Setup repo, explain Docker Compose.
- D1: Create GitHub repo (
-
Day 2:
- D1: Implement
DailyConverterwith exchangerateapi.com and Redis. - D2: Create
Budgetmodel and/api/budgetsendpoint. - D3: Write
pytesttests forDailyConverter.# travelbucks/tests/test_conversion.py import pytest from services.conversion import DailyConverter @pytest.mark.django_db def test_daily_converter(): amount = DailyConverter.convert(100, 'USD', 'JPY') assert isinstance(amount, float) assert amount > 0
- Mentor: Pair program on
DailyConverter.
- D1: Implement
-
Day 3:
- D1: Create
Transactionmodel andTransactionRepo. - D2: Build
/api/transactionsendpoint withBudgetService. - D3: Write
pytesttests for/api/transactions. - Mentor: Explain service layer, review PRs.
- D1: Create
-
Day 4:
- D1: Setup Heroku app (
travelbucks-prod), add PostgreSQL/Redis. - D2: Add indexes to
user_id,created_atin PostgreSQL.CREATE INDEX idx_user_id ON transactions(user_id); CREATE INDEX idx_created_at ON transactions(created_at);
- D3: Write ADR for Heroku (
docs/adr/2025-04-18-use-heroku.md). - Mentor: Host Lunch & Learn on Django models.
- D1: Setup Heroku app (
-
Day 5:
- D1: Setup Sentry for backend error tracking.
# travelbucks/settings.py import sentry_sdk sentry_sdk.init(dsn="your-sentry-dsn", traces_sample_rate=1.0)
- D2: Add JSON logging.
import logging, json_log_formatter formatter = json_log_formatter.JSONFormatter() handler = logging.StreamHandler() handler.setFormatter(formatter) logging.getLogger().addHandler(handler)
- D3: Test backend locally, fix bugs.
- Mentor: Review progress, plan Week 2.
- D1: Setup Sentry for backend error tracking.
Goal: Build React Native UI, integrate with backend, deploy.
-
Day 6:
- D1: Setup React Native with Redux Toolkit and React Query.
// mobile/src/App.js import { Provider } from 'react-redux'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { store } from './store'; import DashboardScreen from './screens/DashboardScreen'; const queryClient = new QueryClient(); export default function App() { return ( <Provider store={store}> <QueryClientProvider client={queryClient}> <DashboardScreen /> </QueryClientProvider> </Provider> ); }
- D2: Build
LoginScreenwith email/password form.// mobile/src/screens/LoginScreen.js import { useState } from 'react'; import { TextInput, Button, View } from 'react-native'; import axios from 'axios'; const LoginScreen = () => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const login = async () => { try { await axios.post('http://localhost:8000/api/login', { email, password }); alert('Logged in!'); } catch (e) { alert('Login failed'); } }; return ( <View> <TextInput placeholder="Email" onChangeText={setEmail} /> <TextInput placeholder="Password" onChangeText={setPassword} secureTextEntry /> <Button title="Login" onPress={login} /> </View> ); };
- D3: Write Jest tests for
LoginScreen.// mobile/src/screens/__tests__/LoginScreen.test.js import { render, fireEvent } from '@testing-library/react-native'; import LoginScreen from '../LoginScreen'; test('submits login form', () => { const { getByPlaceholderText, getByText } = render(<LoginScreen />); fireEvent.changeText(getByPlaceholderText('Email'), 'test@example.com'); fireEvent.changeText(getByPlaceholderText('Password'), 'password'); fireEvent.press(getByText('Login')); // Add mock axios to test API call });
- Mentor: Explain Redux, review PRs.
- D1: Setup React Native with Redux Toolkit and React Query.
-
Day 7:
- D1: Build
DonutChartcomponent withreact-native-chart-kit. - D2: Create
DashboardScreenwithFlatListfor transactions. - D3: Write Jest tests for
DonutChart. - Mentor: Pair program on
DashboardScreen.
- D1: Build
-
Day 8:
- D1: Implement currency setup screen (search + flags).
// mobile/src/screens/CurrencySetupScreen.js import { useState } from 'react'; import { TextInput, Button, View } from 'react-native'; import { useDispatch } from 'react-redux'; import { setRates } from '../store/ratesSlice'; const CurrencySetupScreen = () => { const [homeCurrency, setHomeCurrency] = useState('USD'); const [foreignCurrency, setForeignCurrency] = useState('JPY'); const dispatch = useDispatch(); const save = () => { dispatch(setRates({ [foreignCurrency]: 1 })); // Mock rates alert('Currencies saved'); }; return ( <View> <TextInput placeholder="Home Currency (e.g., USD)" onChangeText={setHomeCurrency} /> <TextInput placeholder="Foreign Currency (e.g., JPY)" onChangeText={setForeignCurrency} /> <Button title="Save" onPress={save} /> </View> ); };
- D2: Build budget setup screen (categories, amounts).
- D3: Write ADR for GDPR (
docs/adr/2025-04-25-gdpr-compliance.md). - Mentor: Host Lunch & Learn on React Native navigation.
- D1: Implement currency setup screen (search + flags).
-
Day 9:
- D1: Add transaction CRUD form to
DashboardScreen.// mobile/src/components/TransactionForm.js import { useState } from 'react'; import { TextInput, Button, View } from 'react-native'; import axios from 'axios'; const TransactionForm = () => { const [amount, setAmount] = useState(''); const [currency, setCurrency] = useState('JPY'); const [category, setCategory] = useState('Food'); const addTransaction = async () => { await axios.post('http://localhost:8000/api/transactions', { amount: parseFloat(amount), from_currency: currency, to_currency: 'USD', category }); setAmount(''); }; return ( <View> <TextInput placeholder="Amount" onChangeText={setAmount} keyboardType="numeric" /> <TextInput placeholder="Currency" onChangeText={setCurrency} /> <TextInput placeholder="Category" onChangeText={setCategory} /> <Button title="Add Transaction" onPress={addTransaction} /> </View> ); };
- D2: Setup Sentry for frontend errors.
// mobile/src/index.js import * as Sentry from '@sentry/react-native'; Sentry.init({ dsn: 'your-sentry-dsn' });
- D3: Write Detox E2E tests for login → transaction flow.
- Mentor: Review frontend integration.
- D1: Add transaction CRUD form to
-
Day 10:
- D1: Deploy to Heroku, test production API.
- D2: Fix bugs, polish UI (e.g., add retry for failed API calls).
- D3: Update
README.mdwith deployment steps. - Mentor: Demo app, prepare for launch.
- Daily Check-Ins: 15 minutes to discuss blockers.
- Pair Programming: 1-hour session weekly (e.g., Day 2 for
DailyConverter). - Lunch & Learn: 30 minutes weekly (Week 1: Django models; Week 2: React Native navigation).
- Notion Guide: Create “TravelBucks Dev Guide” with tutorials (e.g., Django REST Framework, React Native).
- PR Template:
**PR Checklist**: - [ ] Logic in services (backend) or hooks (frontend) - [ ] Tests included - [ ] Linter passes (flake8, eslint) - [ ] API docs updated (if endpoint changed)
- Process: Mentor reviews PRs, provides clear feedback (e.g., “Move this to
BudgetService”).
- API-Client Drift: Generate TypeScript interfaces with
openapi-generator.openapi-generator-cli generate -i backend/swagger.yaml -g typescript-axios -o mobile/src/api
- Rate Fetch Failures: Fallback to last-known rates in Redux.
// mobile/src/services/api.js export const fetchRates = async () => { try { const response = await api.get('/rates'); store.dispatch(setRates(response.data)); return response.data; } catch (e) { return store.getState().rates.rates; // Fallback } };
- Time-Zone Bugs: Store
created_atin UTC, convert withdate-fns.import { format } from 'date-fns-tz'; const utcDate = new Date('2025-04-18T10:00:00Z'); const localDate = format(utcDate, 'yyyy-MM-dd HH:mm:ss', { timeZone: 'Asia/Tokyo' });
GitHub Actions:
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13
env:
POSTGRES_DB: travelbucks
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
ports: ["5432:5432"]
redis:
image: redis:6
ports: ["6379:6379"]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with: { python-version: '3.10' }
- run: pip install -r backend/requirements.txt
- run: python backend/manage.py migrate
- run: pytest backend --cov --cov-fail-under=80
- run: npm install --prefix mobile && npm test --prefix mobile
deploy:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v3
- name: Deploy to Heroku
uses: akhileshns/heroku-deploy@v3.12.12
with:
heroku_api_key: ${{ secrets.HEROKU_API_KEY }}
heroku_app_name: "travelbucks-prod"
heroku_email: "team@example.com"Testing:
- Unit Tests:
pytest(Django),Jest(React Native). - Integration Tests: Test API endpoints with PostgreSQL.
- E2E Tests: Detox for login → transaction flow (Day 9).
- Coverage: 80% minimum.
- README.md:
# TravelBucks Multi-currency budgeting app for travelers. ## Quick Start 1. Clone: `git clone https://github.com/your-org/travelbucks.git` 2. Backend: `cd backend && docker-compose up` 3. Frontend: `cd mobile && npm install && npm start` 4. Env: Add `EXCHANGERATE_API_KEY` to `backend/.env` ## Deployment - Heroku app: `travelbucks-prod` - Add-ons: PostgreSQL, Redis
- Architecture.md:
@startuml actor User User --> [React Native] [React Native] --> [Django API] [Django API] --> [Service Layer] [Service Layer] --> [Repository] [Repository] --> [PostgreSQL] [Service Layer] --> [Redis] [Service Layer] --> [exchangerateapi.com] @enduml
- API Docs: Swagger UI at
/docs/.# travelbucks/urls.py from rest_framework import permissions from drf_yasg.views import get_schema_view from drf_yasg import openapi schema_view = get_schema_view( openapi.Info(title="TravelBucks API", default_version='v1'), public=True, permission_classes=(permissions.AllowAny,), ) urlpatterns = [ path('docs/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), # other URLs ]
- ADRs:
docs/adr/2025-04-18-use-heroku.mddocs/adr/2025-04-25-gdpr-compliance.md
- GDPR:
- Encrypt passwords with
make_password. - Add privacy policy in app (e.g., “We store email and transactions securely”).
- Allow account deletion via settings (post-MVP).
# travelbucks/views/user.py from django.contrib.auth.hashers import make_password class UserViewSet(ModelViewSet): def create(self, request): request.data['password'] = make_password(request.data['password']) return super().create(request)
- Encrypt passwords with
- PCI-DSS: Plan for Stripe (post-MVP) using SDK to avoid storing card data.
- ADR:
# ADR 002: GDPR Compliance **Date**: 2025-04-25 **Decision**: Encrypt passwords, add privacy policy, plan for account deletion. **Rationale**: Protects user data, complies with GDPR.
Action: Consult a legal expert before launch.
- Backend: Sentry for errors, JSON logging for analysis.
- Frontend: Sentry for JavaScript errors, Firebase Crashlytics for crashes.
- Fallback UI: Retryable errors (e.g., “Failed to load rates. Retry?”).
// mobile/src/screens/DashboardScreen.js import { Button } from 'react-native'; const DashboardScreen = () => { const { data, error, refetch } = useQuery(['transactions'], fetchTransactions); if (error) return <Button title="Retry" onPress={refetch} />; // Render dashboard };
- Immediate:
- Day 1: D1 sets up repo, D2 starts login, D3 writes
README.md. - Mentor: Join daily check-ins, review PRs, host Lunch & Learn.
- Day 1: D1 sets up repo, D2 starts login, D3 writes
- Post-MVP:
- Add dark mode, filters.
- Implement Stripe for premium features.
- Enhance GDPR with account deletion.
Resources:
Contact: Mentor available for questions via Slack/email.
Let’s build TravelBucks and help travelers like Alex stay on budget!