Project: Tenant Management: An Evolutionary Project
Evolution: Evolution 2: Modular Architecture
Focus: Modular Monolith
Status: ✅ Complete
Building on our modular monolith evolution, today I want to dive deep into the system architecture of the Tenant Management System. This post explores the architectural decisions, component relationships, and design patterns that make this modular monolith scalable and maintainable.
Evolution Context: This post is part of Evolution 2: Modular Architecture in the Tenant Management Evolutionary Project. This evolution focuses on system architecture and design patterns, building upon the modular monolith foundation established in the previous evolution.
Requirements Context: This system architecture is designed to fulfill the requirements outlined in Landlord-Tenant Management System: Requirements and Objectives. The requirements post provides the business context and user stories that informed these architectural decisions.
The Tenant Management System follows a modular monolith architecture with clear separation of concerns while maintaining a single deployment unit and shared database.
graph TB
subgraph "Client Layer"
WEB[React Frontend]
API_CLIENT[API Clients]
end
subgraph "Application Layer"
FLASK[Flask Backend]
FASTAPI[FastAPI Backend]
end
subgraph "Service Layer"
TENANT_SVC[Tenant Service]
PROPERTY_SVC[Property Service]
TRANSACTION_SVC[Transaction Service]
REPORT_SVC[Report Service]
end
subgraph "Data Access Layer"
MODELS[SQLAlchemy Models]
MIGRATIONS[Database Migrations]
end
subgraph "Data Layer"
SQLITE[(SQLite Database)]
BACKUP[Database Backup]
end
WEB --> FLASK
WEB --> FASTAPI
API_CLIENT --> FLASK
API_CLIENT --> FASTAPI
FLASK --> TENANT_SVC
FLASK --> PROPERTY_SVC
FLASK --> TRANSACTION_SVC
FLASK --> REPORT_SVC
FASTAPI --> TENANT_SVC
FASTAPI --> PROPERTY_SVC
FASTAPI --> TRANSACTION_SVC
FASTAPI --> REPORT_SVC
TENANT_SVC --> MODELS
PROPERTY_SVC --> MODELS
TRANSACTION_SVC --> MODELS
REPORT_SVC --> MODELS
MODELS --> SQLITE
SQLITE --> BACKUP
%% Styling
classDef frontend fill:#4fc3f7,stroke:#0277bd,stroke-width:3px,color:#000
classDef backend fill:#66bb6a,stroke:#2e7d32,stroke-width:3px,color:#fff
classDef database fill:#42a5f5,stroke:#1565c0,stroke-width:3px,color:#fff
classDef external fill:#ab47bc,stroke:#6a1b9a,stroke-width:3px,color:#fff
class WEB,API_CLIENT frontend
class FLASK,FASTAPI,TENANT_SVC,PROPERTY_SVC,TRANSACTION_SVC,REPORT_SVC,MODELS,MIGRATIONS backend
class SQLITE,BACKUP database
1. Layered Architecture The system follows a clean layered architecture with clear separation of concerns:
# Layered structure
├── Presentation Layer (React Frontend)
├── API Layer (Flask/FastAPI Routes)
├── Service Layer (Business Logic)
├── Data Access Layer (SQLAlchemy Models)
└── Data Layer (SQLite Database)
2. Service Layer Pattern Business logic is encapsulated in service classes:
# Service layer implementation
class TenantService:
@staticmethod
def get_tenant_by_id(tenant_id):
"""Get tenant by ID with error handling."""
tenant = Tenant.query.get(tenant_id)
if not tenant:
raise ValueError(f"Tenant with ID {tenant_id} not found")
return tenant
@staticmethod
def create_tenant(data):
"""Create new tenant with validation."""
# Business logic for tenant creation
tenant = Tenant(**data)
db.session.add(tenant)
db.session.commit()
return tenant
3. Dual Backend Implementation Both Flask and FastAPI backends provide the same functionality:
# Flask implementation
@api.route('/tenants', methods=['GET'])
def get_tenants():
try:
tenants = TenantService.get_all_tenants()
return jsonify(tenants)
except Exception as e:
return jsonify({'error': str(e)}), 500
# FastAPI implementation
@app.get("/tenants", response_model=List[TenantOut])
def get_tenants(db: Session = Depends(get_db)):
return TenantService.get_all_tenants(db)
Component Structure:
frontend/src/
├── components/
│ ├── Navigation.js # Main navigation
│ ├── Dashboard.js # Dashboard overview
│ ├── Tenants.js # Tenant management
│ ├── Properties.js # Property management
│ ├── Transactions.js # Transaction management
│ └── modals/ # Modal components
│ ├── TenantDetailsModal.js
│ ├── TenantTransactionsModal.js
│ └── PropertyTransactionsModal.js
├── App.js # Main application
└── App.css # Global styles
State Management:
// Component-level state management
const Tenants = () => {
const [tenants, setTenants] = useState([]);
const [loading, setLoading] = useState(false);
const [properties, setProperties] = useState([]);
// API integration
const fetchTenants = useCallback(async () => {
setLoading(true);
try {
const res = await axios.get(`/api/tenants?page=${page}&per_page=${perPage}`);
setTenants(res.data.tenants || []);
} catch (error) {
toast.error('Failed to fetch tenants');
} finally {
setLoading(false);
}
}, [page, perPage]);
};
Flask Backend Structure:
backend/
├── app.py # Application factory
├── config.py # Configuration management
├── models.py # Database models
├── routes.py # API routes
├── services.py # Business logic
└── swagger.py # API documentation
FastAPI Backend Structure:
fastapi_backend/
├── main.py # FastAPI application
├── config.py # Configuration
├── database.py # Database connection
├── models.py # Database models
└── schemas.py # Pydantic schemas
The modular monolith uses a single SQLite database shared across all modules:
# Shared database configuration
DATABASE_URI = os.getenv('DATABASE_URI', 'sqlite:///app.db')
# All services use the same database
class Tenant(Base):
__tablename__ = 'tenant'
# ... tenant fields
class Property(Base):
__tablename__ = 'property'
# ... property fields
class Transaction(Base):
__tablename__ = 'transaction'
# ... transaction fields
erDiagram
TENANT ||--o{ TRANSACTION : has
PROPERTY ||--o{ TENANT : contains
PROPERTY ||--o{ TRANSACTION : has
TENANT {
int id PK
string name
int property_id FK
string passport
date passport_validity
string aadhar_no
string employment_details
string permanent_address
string contact_no
string emergency_contact_no
float rent
float security
date move_in_date
date contract_start_date
date contract_expiry_date
}
PROPERTY {
int id PK
string address
float rent
float maintenance
}
TRANSACTION {
int id PK
int property_id FK
int tenant_id FK
string type
string for_month
float amount
date transaction_date
string comments
}
Consistent API endpoints across both backends:
API Endpoints:
Tenants:
GET /api/tenants # List tenants
GET /api/tenants/{id} # Get specific tenant
POST /api/tenants # Create tenant
PUT /api/tenants/{id} # Update tenant
DELETE /api/tenants/{id} # Delete tenant
Properties:
GET /api/properties # List properties
GET /api/properties/{id} # Get specific property
POST /api/properties # Create property
PUT /api/properties/{id} # Update property
DELETE /api/properties/{id} # Delete property
Transactions:
GET /api/transactions # List transactions
GET /api/transactions/{id} # Get specific transaction
POST /api/transactions # Create transaction
PUT /api/transactions/{id} # Update transaction
DELETE /api/transactions/{id} # Delete transaction
Reports:
GET /api/reports/tenants_csv # Export tenants CSV
GET /api/reports/properties_csv # Export properties CSV
GET /api/reports/transactions_csv # Export transactions CSV
GET /api/backup # Database backup
Both backends expose identical APIs with different implementations:
# Flask route
@api.route('/tenants', methods=['GET'])
def get_tenants():
try:
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
tenants = Tenant.query.paginate(
page=page, per_page=per_page, error_out=False
)
return jsonify({
'tenants': [tenant.to_dict() for tenant in tenants.items],
'total': tenants.total,
'pages': tenants.pages,
'current_page': tenants.page
})
except Exception as e:
return jsonify({'error': str(e)}), 500
# FastAPI route
@app.get("/tenants", response_model=List[TenantOut])
def get_tenants(
page: int = Query(1, ge=1),
per_page: int = Query(10, ge=1, le=100),
db: Session = Depends(get_db)
):
# Same functionality, different implementation
return TenantService.get_all_tenants(db)
# Option 1: Flask + React
uv run python run.py # Flask backend (port 5000)
cd frontend && npm start # React frontend (port 3000)
# Option 2: FastAPI + React
uv run uvicorn fastapi_backend.main:app --reload # FastAPI backend (port 8000)
cd frontend && npm start # React frontend (port 3000)
# Environment-based configuration
class Config:
DATABASE_URI = os.getenv('DATABASE_URI', 'sqlite:///app.db')
CORS_ORIGINS = os.getenv('CORS_ORIGINS', 'http://localhost:3000').split(',')
SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key')
@staticmethod
def init_app(app):
app.config['SQLALCHEMY_DATABASE_URI'] = Config.DATABASE_URI
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
1. Clean Separation Without Complexity
2. Development Efficiency
3. Future Microservices Path
Advantages:
Limitations:
graph LR
subgraph "Current: Modular Monolith"
A[Single App]
B[Shared DB]
end
subgraph "Future: Microservices"
C[Tenant Service]
D[Property Service]
E[Transaction Service]
F[Notification Service]
G[Tenant DB]
H[Property DB]
I[Transaction DB]
J[Notification DB]
end
A --> C
A --> D
A --> E
A --> F
B --> G
B --> H
B --> I
B --> J
%% Styling
classDef current fill:#ff7043,stroke:#d84315,stroke-width:3px,color:#fff
classDef future fill:#66bb6a,stroke:#2e7d32,stroke-width:3px,color:#fff
classDef database fill:#42a5f5,stroke:#1565c0,stroke-width:3px,color:#fff
class A,B current
class C,D,E,F future
class G,H,I,J database
# Service extraction example
class TenantService:
# Current: In-memory service
def get_tenant(self, tenant_id):
return Tenant.query.get(tenant_id)
# Future: HTTP service call
def get_tenant(self, tenant_id):
response = requests.get(f"{TENANT_SERVICE_URL}/tenants/{tenant_id}")
return response.json()
You can explore the architectural decisions in the codebase:
tenant-management-modular/services.pyroutes.pymodels.pyThe modular monolith architecture provides the perfect balance between simplicity and structure. It gives us clean separation of concerns without the operational complexity of microservices, making it an excellent stepping stone for learning clean architecture principles.
The dual backend implementation (Flask + FastAPI) demonstrates how the same business logic can be exposed through different frameworks, providing valuable insights into API design and framework comparison.
This system architecture analysis represents the culmination of Evolution 2:
The modular monolith architecture provides the perfect balance between simplicity and structure. It demonstrates how clean architecture principles can be applied within a single deployment unit, making it an excellent stepping stone for learning enterprise patterns. This foundation prepares the system for the next evolutionary step toward enterprise-ready architecture.
Next up: I’ll dive deeper into the component architecture, exploring the internal design of individual modules and how they implement the business logic - stay tuned!